Skip to content

Instantly share code, notes, and snippets.

@mjstevens777
Last active October 18, 2024 18:41
Show Gist options
  • Save mjstevens777/e6626e1c764f8ba80db0bb8649bf910b to your computer and use it in GitHub Desktop.
Save mjstevens777/e6626e1c764f8ba80db0bb8649bf910b to your computer and use it in GitHub Desktop.
CS:2 River Guide

CS:2 River Guide

Are you sick of rivers overflowing and flooding everything? Use this guide to understand how rivers work in Cities Skylines 2.

For a stream source, you can use the formula:

$Flow = 0.057 \times Area \times Grade$

For the most part, radius doesn't matter.

For example, a river that's 20 meters deep, 100 meters wide, and flows at a 1% grade should have

$Flow = 0.057 \times 20 \times 100 \times 0.01 = 1.14$

This should fill the river all the way to the top, so you may want to lower the number a bit to leave some wiggle room.

Scaling

You can also use this rule to match the flow between different sections of a river:

  • For any combination of X, Y = wide, deep, or steep
    • If you make the river twice as X, make it half as Y
    • If you make the river twice as X, give it twice as much flow
  • For example
    • If you make the river twice as wide, make it half as steep
    • If you make the river half as steep, make it twice as deep
    • If you give the river twice as much flow, make it twice as deep

Calculating Area

The area of the river channel is $Width \times Depth$. For depth you can count contour lines or use another tool. For the most accurate width, measure halfway up the river bank.

Calculating Grade

Draw a road along the river's direction of flow to get the grade. Divide the grade by 100 when using the formula.

Real Life Comparison

Since a lot of us have intuitions of what rivers should look like based on real life, we can compare how the game is different.

The formula above talks about flow, but $Flow = Speed \times Area$, so if we use an arbitrary constant $k$, we can simplify the Cities Skylines behavior to just

$Speed = k \times Grade$

In real life, the Manning formula says* that

$Speed = k \times \sqrt[3]{Area} \times \sqrt{Grade}$

so the Cities Skylines version is a lot simpler. In real life, bigger rivers flow faster because they have less friction. So big rivers in Cities Skylines flow a little slower than they should. Also in real life, steep rivers experience a lot more friction than flatter rivers (since friction increases with velocity squared), so steep rivers in Cities Skylines flow faster than they should and carry more water than they should.

(*This assumes that roughly $Area = (Hydraulic\ Radius)^2$)

Simulation Under the Hood

Based on the observed results, here is a likely explanation for how the water simulation works.

Imagine you have two cells next to each other. Each cell has a water level W and a ground level G. And between the cells you have a volumetric flow rate Q.

We observed $Speed = k \times Grade$ before, so we can set up the speed using a finite difference:

$V_{12} = k \times (W_2 - W_1)$

(Side note - there's nothing stopping the game devs from using the Manning formula $V_{12} = k \times \sqrt{W_2 - W_1}$ here)

Then, to ensure conservation of mass, we turn speed into flow by multiplying the water depth:

$Q_{12} = V_{12} \times (W_1 - G_1)$

$Q_{12} = k \times (W_2 - W_1) \times (W_1 - G_1)$

And if we define a stream source, it just injects the same amount of water into the cell every second, which we can call $Q_{src}$

So there are three flows we need to keep track of - water flowing in from the left, water flowing out to the right, and water generated by a source. If more water is flowing in than flowing out, the water level will rise. So we get an update rule:

$W_{new} - W_{old} = Q_{in} - Q_{out} + Q_{src}$

For cell 2, this comes out to:

$W_{new} = W_{old} + Q_{12} - Q_{23} + Q_{src}$

And then this simulation is super simple to run for all of the cells in a grid in parallel on a GPU.

Methodology

I used a test map to calibrate the flow rates. River banks are at a 45 degree angle / 100% grade, and the bottom of the river channel is flat. I used increments of 4 heightmap pixels, equal to 14 meters, for testing. I tested at 1%, 2%, 4%, 6%, and 8% grades. I added a buffer of 140 meters between rivers and added a 5% grade in case the river overflowed a little.

Here are the height maps I generated - one local heightmap and one world heightmap.

Then I added water sources and tuned the flow until they just barely hit the top of the rivver

testbed

Data

Here is the raw data I generated

Grade (%) Width (m) Depth (m) Area (sq. m) Full Width (m) Flow
1 42 14 588 56 0.35
1 98 14 1372 112 0.8
1 84 28 2352 112 1.45
1 196 28 5488 224 3.2
1 168 56 9408 224 5.5
1 392 56 21952 448 11.5
1 336 112 37632 448 21
2 42 14 588 56 0.65
2 98 14 1372 112 1.5
2 84 28 2352 112 2.7
4 42 14 588 56 1.4
4 98 14 1372 112 3.4
6 42 14 588 56 2
6 98 14 1372 112 4.5
8 42 14 588 56 2.9
8 98 14 1372 112 5.8

Analysis

I fit the data using the formula at the top of the page, and the fit was very good. Here is a residual plot of all the data:

And here is a plot of flow vs. area by grade

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment