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.
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.