Dev Log
Environmental Simulation Part 2
This post is a continuation of a earlier post - find it here.
Continuing the journey of an environmental simulation we have now come to the hard part. Or at least the part that has been giving me fits for the past several weeks.
Water / Rain Cycle
Creating the water cycle was a fair amount of additional code. Most of what is described in the previous post is controlled by two scripts - a climate manager and a data containing script attached to each climate block. Adding the ability for water to get transported by clouds added just a couple more scripts but plenty more interactivity between those scripts. Which makes for so many more potential bugs, wrong ideas and as it turns out a few floating point issues.
The Basic Idea
Water is stored in the ground as (shocking I know) "ground water." This will represent water that plants and crops can use to grow - with plants having ideal or preferred conditions that allow optimal growth. Ground water flows between neighboring climate blocks and evaporates depending on temperature. All evaporation and ground water flow stop if the temperature is below freezing - not totally physically accurate, but maybe to a first approximation it is... I'm not about to start modeling sublimation! This causes water to build up during the winter and provides wet soils for spring crops. All in all this matches with what I want to accomplish, but will likely need lots of tweaks and balancing.
The final step to the water cycle was to enable clouds that could carry water while being pushed by the seasonal winds. The ground water or ocean water evaporates and generates localized humidity. When this humidity hits a cloud formation threshold a cloud is spawned and the moisture contained in the humidity is transferred to the cloud. The cloud is then free to ride the winds and will begin to drop is water when it certain conditions for a climate block are met - essentially cumulative humidity plus cloud water content equals a given threshold.
Getting the basic mechanics up and running was fairly easy. Isn't that true of most game design elements?
The struggle came in balancing the cycle. Now it wouldn't be hard to simply have the ground water fluctuate with the seasons, but I want more variability than that solution could provide. I want rain shadows from hills, I want the coastal regions to be wetter, I want localized flooding, I want rain fall to be tied into larger climate factors... I want a simulation rather than hard coded weather or totally random weather.
Challenge #1 - Cloud movement
Getting the clouds to move over land and drop water has been tricky. If the cloud drops all of its water back in the ocean that's okay, but if every cloud does that then there's no rain... Creating a on and off shore breeze was key to this. Hot ground with cool atmosphere above it causes an on shore breeze, while cool ground tends to cause off shore breezes.
Challenge #2 - Water Return
Once the water has made it to the ground it then needs to cycle out via evaporation (cloud formation) or flowing back to the ocean. For simplicity (haha, I can anyone say over scoped?) I don't have rivers in the terrain. So they aren't an avenue for the water. The water must flow through a climate block's neighbors until it reaches the ocean.
Challenge #3 - Dynamic Balance
Since the world should react to the players actions, but do so in a way that is "reasonable" the water cycle needs to somehow dynamically balance itself. I think the fancy term for this is "stability." Yes I want the water cycle to speed up or slow down, but it shouldn't be unstable - the "butterfly effect" is cool and all but makes for lousy game play. So far most simulations have been very unstable with tendencies towards drought or flood being very common. Hmm.
Idea #1 : Open System - Endless Water!
At first I tried an open water system. The ocean for my purposes would be an infinite source and sink of water. After every cycle the water content of each ocean climate block would be reset. This seemed to make sense as the amount of evaporation from the ocean would not be heavily dependent on how soaked the ground might be (since evaporation rate is dependent on ground water content). This led to a highly unstable system. Small tweaks in variables would cause massive and consistent flooding. While other tweaks would cause complete drought. Not good.
To add to the struggle a year in-game takes about 5 hours to simulate (at current settings). So Each major or minor tweak required 10+ hours (2 in-game years) to see what difference had been made. Ugh.
Why did I choose this game? I keep telling myself either "I'm an idiot" or "good things only come with hard work." The longer this balancing takes the more I tend to hear the first message.
Idea #2 : Closed System
With plenty of moaning, groaning and head scratching I tried desperately to find a new mechanism for water transport - not different from clouds and rain, but different in the mathematical sense.
I realized that my ground water flow simply wasn't cutting it. It wasn't moving water fast enough and it was generally moving the system away from stability (flood or drought).
The new model has 3 significant changes to the water cycle.
- The system is now closed. The ocean will not visually dry out, but the amount of water in the world is now constant - no more infinite source or sink. So as ocean water evaporates it is not replaced until it rains or water flows from the ground back to the ocean. There is a few lines of code that check the total water in the system and add and subtract as needed - just in case.
- There is now a "target" for ground water. A PID controller (overkill?) adjusts the water flow rate for each climate block to slowly return the ground water content to a target value. I don't like having this, but it seems to add stability.
- An adjustable portion of the ground water flow now returns directly to the ocean no matter where that climate block is located. This required that each ocean block is not independent but shares a large pool (no pun intended) of ocean water. This functions as underground streams which are a real thing or so I've been told.
At first blush this approach didn't work. The result was still an unstable system quickly alternating between floods and droughts. To add to the challenge the system wasn't actually closed... Oops.
Idea #3 - Closed System with New Water Transport
At this point I was ready to give up. Really and truly. It had been weeks of struggle with little that would work in a commercial game (even a small time indie one). I took a little time off while I ran 12-24 hour long simulations - I was irrationally hopping to find stability with small tweaks in parameters.
Once I pulled my head out of my butt I started to rethink the cause of the system not being closed and how I transported the water.
The cloud mechanism made a lot of sense as water transport, but it added substantial complexity and inter-connectivity that I wasn't excited about. The clouds had the added bummer of being physics based which needs to run on the main thread in Unity. This not only results in a (slight) slow down, but a decent amount of interplay between the a "cloud thread" and the main thread that's running the physics engine. This seemed like a very good potential candidate for causing the system to not be closed - errors given the complexity and syncing data between threads.
I rewrote the water transport code to get rid of the cloud and to use humidity to move water from neighbor to neighbor based on relative levels of humidity as well as the direction of the wind. This allows humidity to both spread around the map and to move with the wind. Since only the direction and magnitude of the wind is needed all of these calculations could be run off the main thread. Nice bonus!
I ran a few simulations with this new mechanism and was generally happy with the result, except the system was still not closed. After 30-60 minutes there was a noticeable error in the total amount of water in the system... Shit.
The errors per simulation cycle were tiny. Almost not there. I tracked down a few to simple small mistakes in coding. Simple mistakes that with larger numbers would have shown up much earlier and easier - when you're changing a value in the 6th or 7th decimal it can be hard to see the problem. Some of the others were harder to find.
Some of the error was simple floating point errors. Most values in the simulation are between 0 and 1, but a few such as the total water are in the hundreds. Now I don't claim to be smart enough to fully and deeply understand floating point errors, but I know they exist and I could see them creeping into my simulation. The errors in some of the larger values were bigger than the changes in other values...
Since I'm targeting PC (and maybe OSX) I'm not too worried about memory - at least not yet - I switched the whole simulation to double precision floats.
AND?
At long last the simulation was stable. Not perfect. Not balanced. But stable. I ran the simulation for over 30,000 cycles and saw an accumulated error of 0.00001 in a value that was close to 1000. I may have danced a little when this happened. Which led to the most wonderfully boring GIF I've made (watch that last digit change).
I have a dream that the hard part of creating the simulation is over. Let me have my dreams!
So what remains?
- Tweaks and balancing
- Pollution and movement of pollution
- "Green house" effects from clouds and CO2
In reverse order... The green house effects are already "coded" but currently have no effect (multiplying by 1 type of thing). So adding that functionality might fall under the tweaks and balancing as much as anything. Not too worried about that aspect. I am interested to see how the cloud based green house effect will effect the system. The wind flows from hot to cold and clouds will have a tendency to insulate. So areas shouldn't get as warm or get as cold. Hmm. Maybe a net zero effect?
The CO2 will of course have a similar effect, but will be global rather than local like the clouds. The difference with the CO2 effect is it has a lot of interacting parts. There will be several CO2 sources and several sinks. Once again getting that balance could be difficult. The first phase of the game is unlikely to have substantial sources of CO2 so full implementation of the greenhouse effect may wait until large industrial structures are available.
Pollution will just be a double precision float variable like ground water. My plan at the moment is simple, have pollution move with ground water. A percent of the pollution will move with each cycle and spread to neighboring climate blocks as well as into the ocean. There will be some decay of pollution - i.e. it just goes away. How exactly that will work is unclear. My thinking is that pollution will accumulate in the ocean and break down while there. This leaves the door open to allow pollution to make its way to the player and NPCs via drinking water and eating fish or other food containing pollution. We'll see.
With the simulation now stable, I'll be turning my attention back to creating a playable game loop. There'll likely be a "Part 3" to this series of posts, but that might be a while.
Â
Environmental Simulation Part 1
When I settled on the current game design idea a central piece was an environment simulation that would effect player experiences and that player choices could in turn effect - creating a feedback cycle.
The goal wasn't (and still isn't) to create an environmental simulation that was "physically accurate." The goal was to create a simulation that was an abstraction of reality or at least had some of the key features seen day to day and year to year. I wanted the player to be able to alter the world around them in a way that reflects reality but that doesn't need to be 100% realistic.
Key Features:
- Day to day temperature swings
- Seasonal temperature swings
- Wind to transport moisture
- Water/Rain cycle
- CO2 effects
- Pollution accumulation and transport
- Runs off the main thread
Building Blocks
The first step in creating the simulation was to break up the environment into a series of blocks. Each box would have it's own set of environmental variables (temperature, humidity, ground water, wind, etc). These "climate blocks" would interact with it's neighbors to create localized weather and also to allow local variances such as terrain height and cloud density to impact the weather.
As the simulation developed and more complex interactions were add a few changes/additions to the climate blocks were needed. The first critical piece was the addition of an "atmosphere block" on top of each climate block. The atmosphere block is simulated air space above that often has a different temperature than the air on the ground/ocean. This allowed for up and down drafts that could help move clouds and cause an on or off shore wind depending on the season (more about this later). Without an onshore wind clouds and more importantly rain rarely make it to the land.
A second key addition was the classification of terrain types. At the time of writing this the simulation is only using three types of terrain ocean, grassland and mountains. This largely controls the "target" temperature of climate block and in the case of the ocean maintains a constant saturation of ground water. Oceans have moderate target temps with smaller seasonal and daily fluctuations. Grasslands have a higher target temp than the mountains but both have larger daily and seasonal fluctuations than the ocean.
Daily and Seasonal Temperatures
Seasonal sun intensity is controlled by an animation curve that is sinusoidal in shape. Max values occurring during the "summer" and of course minimum values occurring during the "winter." A second curve was used for daily fluctuations. The curves are currently identical... That may change in the future.
At first these curves were used and tuned to be a true energy flux with energy being added during the day and energy lost during the night. While more physically accurate and potentially allowing for more emergent weather, it also made it far more difficult to tune and balance. After significant testing and tuning this method was abandoned for a more controlled method.
Each terrain type has a median temperature that serves as a target temperature. There are also values for seasonal and daily fluctuations of that temperature. This means that median temperatures + seasonal flux + daily flux is roughly the high temp and likewise median temperature - seasonal flux - daily flux is roughly the low temp. Variance from this is caused by the connection of neighbors and other factors.
One of the other factors that adds some variability is the average height of the terrain. In the real world the higher the terrain the cooler the air. To reflect this each climate block samples the terrain height at multiple points and calculates an average height. From this the median temperature is adjusted slightly. This provides variation in the climate that (hopefully) lends itself to more interesting and emergent weather.
Wind
Wind in the simulation does not have a physical presence and is simply a Vector3 that stores the wind velocity. The wind for an individual climate block is calculated based on the temperatures of the neighboring climate blocks, including the upper atmosphere. The inclusion of the upper atmosphere allows up and down drafts which allows for onshore or offshore breezes (as mentioned above).
To calculate the wind the simulation calculates a temperature difference between the climate block and a neighbor and then multiplies that by a vector that goes from the climate block to the neighbor. Final direction (sign) of the wind is based on blowing from hot to cold.
The simulation sums all the winds due to neighbors to arrive at a (nearly) final wind value.
A prevailing wind is then added. This allows larger global winds to be added to create larger and different weather patterns. The strength of the prevailing wind is a tune-able variable. Current testing is with an in/out pattern that helps to blow clouds away from land in the summer.
After the prevailing wind is added the vector is normalized. There was some debate about normalizing the wind... In the end the choice was made to keep the normalization as this meant that clouds always would have some wind. It also prevents unrealistically large or small winds that become problematic when trying to transport clouds/moisture.
The final step is a check on the vertical component of the wind. Since the upper atmosphere is often at a very different temperature it can therefore be the dominant contribution to the wind. This is a problem due to only simulating two layers of atmosphere rather than more layers or the ideal of a continuous atmosphere. Effectively the vertical component is clamped to a range between -0.5 and 0.5. This helps to keep clouds generally moving horizontally.
Additional controls are needed to avoid the clouds going too high or going through the ground. The altitude of the clouds is mostly visual as the horizontal location determines where clouds can deliver rain. Because of the this a simple min and max altitude are set. Clouds check against these values and degrees of freedom are restricted in the rigidbody to prevent clouds from going outside the bounds. Checks on vertical velocity are also necessary to allow the clouds to move away from the limits.
In Part 2 I'll discuss the most difficult part (so far) of the simulation that being the Water and Rain Cycle. Or more specifically the balancing of that cycle.
Older Posts
-
April 2024
- Apr 10, 2024 Ready for Steam Next Fest? - Polishing a Steam Page Apr 10, 2024
- Apr 1, 2024 Splitting Vertices - Hard Edges for Low Poly Procedural Generation Apr 1, 2024
-
November 2023
- Nov 18, 2023 Minute 5 to Minute 10 - Completing the Game Loop Nov 18, 2023
-
September 2023
- Sep 13, 2023 Visual Debugging with Gizmos Sep 13, 2023
-
July 2023
- Jul 4, 2023 Easy Mode - Unity's New Input System Jul 4, 2023
-
May 2023
- May 19, 2023 Level Builder - From Pixels to Playable Level May 19, 2023
-
April 2023
- Apr 11, 2023 Input Action in the Inspector - New Input System Apr 11, 2023
-
February 2023
- Feb 26, 2023 Tutorial Hell - Why You're There. How to Get Out. Feb 26, 2023
-
December 2022
- Dec 31, 2022 Upgrade System (Stats Part 2) Dec 31, 2022
-
November 2022
- Nov 10, 2022 Stats in Unity - The Way I Do it Nov 10, 2022
- Nov 5, 2022 State of UI in Unity - UI Toolkit Nov 5, 2022
-
August 2022
- Aug 17, 2022 Knowing When A Coroutine Finishes Aug 17, 2022
-
April 2022
- Apr 23, 2022 Unity Input Event Handlers - Or Adding Juice the Easy Way Apr 23, 2022
-
March 2022
- Mar 15, 2022 *Quitting a Job I Love Mar 15, 2022
-
February 2022
- Feb 8, 2022 Split Screen: New Input System & Cinemachine Feb 8, 2022
-
January 2022
- Jan 24, 2022 (Better) Object Pooling Jan 24, 2022
- Jan 19, 2022 Designing a New Game - My Process Jan 19, 2022
- Jan 16, 2022 Strategy Game Camera: Unity's New Input System Jan 16, 2022
-
December 2021
- Dec 16, 2021 Raycasting - It's mighty useful Dec 16, 2021
-
November 2021
- Nov 22, 2021 Cinemachine. If you’re not. You should. Nov 22, 2021
-
August 2021
- Aug 3, 2021 C# Extension Methods Aug 3, 2021
-
June 2021
- Jun 27, 2021 Changing Action Maps with Unity's "New" Input System Jun 27, 2021
-
May 2021
- May 28, 2021 Unity's New Input System May 28, 2021
- May 8, 2021 Bolt vs. C# - Thoughts with a dash of rant May 8, 2021
-
March 2021
- Mar 10, 2021 Coroutines - Unity & C# Mar 10, 2021
-
January 2021
- Jan 14, 2021 Where's My Lunch? - January Devlog Update Jan 14, 2021
-
December 2020
- Dec 27, 2020 C# Generics and Unity Dec 27, 2020
- Dec 7, 2020 Steam Workshop with Unity and Facepunch Steamworks Dec 7, 2020
-
November 2020
- Nov 27, 2020 Simple Level Save and Load System (Unity Editor) Nov 27, 2020
- Nov 9, 2020 Command Pattern - Encapsulation, Undo and Redo Nov 9, 2020
-
October 2020
- Oct 28, 2020 GJTS - Adding Steamworks API and Uploading Oct 28, 2020
- Oct 9, 2020 Game Jam... Now What? Oct 9, 2020
-
August 2020
- Aug 16, 2020 Strategy Pattern - Composition over Inheritance Aug 16, 2020
-
July 2020
- Jul 24, 2020 Observer Pattern - C# Events Jul 24, 2020
- Jul 15, 2020 Object Pooling Jul 15, 2020
- Jul 3, 2020 Cheat Codes with Unity and C# Jul 3, 2020
-
June 2020
- Jun 16, 2020 The State Pattern Jun 16, 2020
-
August 2019
- Aug 12, 2019 Easy UI Styles for Unity Aug 12, 2019
-
July 2019
- Jul 3, 2019 9th Grade Math to the Rescue Jul 3, 2019
-
June 2019
- Jun 12, 2019 Introducing My Next Game (Video DevLog) Jun 12, 2019
-
May 2019
- May 29, 2019 Programming Challenges May 29, 2019
-
March 2019
- Mar 2, 2019 Something New - Asking "What Can I Learn?" Mar 2, 2019
-
November 2018
- Nov 30, 2018 A Growing Channel and a New Tutorial Series Nov 30, 2018
-
October 2018
- Oct 11, 2018 Procedural Spaceship Generator Oct 11, 2018
-
July 2018
- Jul 11, 2018 Implementing SFX in Unity Jul 11, 2018
-
May 2018
- May 31, 2018 Prototyping Something New May 31, 2018
-
April 2018
- Apr 17, 2018 When to Shelve a Game Project? Apr 17, 2018
-
February 2018
- Feb 9, 2018 State of the Game - Episode 3 Feb 9, 2018
-
December 2017
- Dec 16, 2017 State of the Game - Episode 2 Dec 16, 2017
-
November 2017
- Nov 7, 2017 The Bump From A "Viral" Post Nov 7, 2017
-
October 2017
- Oct 30, 2017 NPC Job System Oct 30, 2017
-
September 2017
- Sep 1, 2017 Resources and Resource Systems Sep 1, 2017
-
August 2017
- Aug 3, 2017 State of the Game - Episode 1 Aug 3, 2017
-
June 2017
- Jun 20, 2017 Resources: Processing, Consumption and Inventory Jun 20, 2017
- Jun 15, 2017 Energy is Everything Jun 15, 2017
-
May 2017
- May 16, 2017 Graphing Script - It's not exciting, but it needed to be made May 16, 2017
- May 2, 2017 Tutorials: Low Poly Snow Shader May 2, 2017
-
April 2017
- Apr 28, 2017 Low Poly Snow Shader Apr 28, 2017
- Apr 21, 2017 Environmental Simulation Part 2 Apr 21, 2017
- Apr 11, 2017 Environmental Simulation Part 1 Apr 11, 2017
-
March 2017
- Mar 24, 2017 Building a Farming Game Loop and Troubles with Ground Water Mar 24, 2017
-
February 2017
- Feb 25, 2017 The Inevitable : FTF PostMortem Feb 25, 2017
-
December 2016
- Dec 7, 2016 Leaving Early Access Dec 7, 2016
-
November 2016
- Nov 28, 2016 Low Poly Renders Nov 28, 2016
- Nov 1, 2016 FTF: Testing New Features Nov 1, 2016
-
October 2016
- Oct 27, 2016 Watchtowers - Predictive Targeting Oct 27, 2016
- Oct 21, 2016 Click to Color Oct 21, 2016
- Oct 19, 2016 Unity Object Swapper Oct 19, 2016
-
September 2016
- Sep 18, 2016 Testing Single Player Combat Sep 18, 2016
-
May 2016
- May 25, 2016 Release Date and First Video Review May 25, 2016
-
March 2016
- Mar 26, 2016 Getting Greenlit on Steam Mar 26, 2016