• Announcements

    • khawk

      Download the Game Design and Indie Game Marketing Freebook   07/19/17

      GameDev.net and CRC Press have teamed up to bring a free ebook of content curated from top titles published by CRC Press. The freebook, Practices of Game Design & Indie Game Marketing, includes chapters from The Art of Game Design: A Book of Lenses, A Practical Guide to Indie Game Marketing, and An Architectural Approach to Level Design. The GameDev.net FreeBook is relevant to game designers, developers, and those interested in learning more about the challenges in game development. We know game development can be a tough discipline and business, so we picked several chapters from CRC Press titles that we thought would be of interest to you, the GameDev.net audience, in your journey to design, develop, and market your next game. The free ebook is available through CRC Press by clicking here. The Curated Books The Art of Game Design: A Book of Lenses, Second Edition, by Jesse Schell Presents 100+ sets of questions, or different lenses, for viewing a game’s design, encompassing diverse fields such as psychology, architecture, music, film, software engineering, theme park design, mathematics, anthropology, and more. Written by one of the world's top game designers, this book describes the deepest and most fundamental principles of game design, demonstrating how tactics used in board, card, and athletic games also work in video games. It provides practical instruction on creating world-class games that will be played again and again. View it here. A Practical Guide to Indie Game Marketing, by Joel Dreskin Marketing is an essential but too frequently overlooked or minimized component of the release plan for indie games. A Practical Guide to Indie Game Marketing provides you with the tools needed to build visibility and sell your indie games. With special focus on those developers with small budgets and limited staff and resources, this book is packed with tangible recommendations and techniques that you can put to use immediately. As a seasoned professional of the indie game arena, author Joel Dreskin gives you insight into practical, real-world experiences of marketing numerous successful games and also provides stories of the failures. View it here. An Architectural Approach to Level Design This is one of the first books to integrate architectural and spatial design theory with the field of level design. The book presents architectural techniques and theories for level designers to use in their own work. It connects architecture and level design in different ways that address the practical elements of how designers construct space and the experiential elements of how and why humans interact with this space. Throughout the text, readers learn skills for spatial layout, evoking emotion through gamespaces, and creating better levels through architectural theory. View it here. Learn more and download the ebook by clicking here. Did you know? GameDev.net and CRC Press also recently teamed up to bring GDNet+ Members up to a 20% discount on all CRC Press books. Learn more about this and other benefits here.
Sign in to follow this  
Followers 0
Mekuri

Issues with 2D tile-based Sidescroller Water

13 posts in this topic

Hey there. On my journey through my sidescroller game I've come to the point where I need to add water. I started out today, and very quickly ended up with decent water. There was an issue though- When the water is finished updating (which might take some time depending on the pool size) the water is rarely flat on top. Let me illustrate

What I would like is the water surface to be like this:

------------------------------ <--- surface

But what I might get is something like this:

---______________ <--- Undesirable surface.

Basically the surface is leaning in either direction (thought it appears to be leaning to the right mostly)
I've spend about eight hours working on this, and I am no closer to a good solution.

While searching the web I found this method for handling water:

1.From bottom to top, if a cell which contains water is over a cell which could contain water but is not full, move as much water as can be moved from the upper cell to the lower one.
2.From bottom to top, for each cell which contains water, if the cell below cannot accommodate any more water, transfer 1/4 of the 'starting amount' of water to each side if there's room.

One full tile of water contains 100 units of water. I even tried doing it by transfering 1 unit instead og 1/4.. but this gave extremely undesirable effects, like floating water covering large areas very quickly... strange stuff [img]http://public.gamedev.net//public/style_emoticons/default/tongue.png[/img]

When I draw the water tiles (a tile is 16X16) I draw the depth of each tile like this - 16/100 * waterLevel. So a tile with 50 units of water will be drawn as 8 pixels deep. As you may notice in the code I keep the waterlevel value in bytes, to save memory and world file size.

Anyways, my current code will follow shortly - It is the simplest form I've made based on the method above- And also the most succesful one:

[CODE]
private void UpdateWater(GameTime gameTime)
{
/*_waterTimer -= (float)gameTime.ElapsedGameTime.TotalMilliseconds;
if (_waterTimer > 0)
return;
_waterTimer = 34;*/
for (int x = (int)(_pc.Position.X / StaticTileInfo.Size.X) - UPDATE_RADIUS_X; x < (int)(_pc.Position.X / StaticTileInfo.Size.X) + UPDATE_RADIUS_X; x++)
{
for (int y = (int)(_pc.Position.Y / StaticTileInfo.Size.Y) + UPDATE_RADIUS_Y; y > (int)(_pc.Position.Y / StaticTileInfo.Size.Y) - UPDATE_RADIUS_Y; y--)
{
if (x < 0 || x > tg.World.GetLength(0) || y < 0 || y > tg.World.GetLength(1))
continue;
if (tg.World[x, y].Type != TileType.Water)
continue;
//Water flows downwards
if (tg.World[x, y + 1].Type == TileType.Water)
{
if (((FluidTile)tg.World[x, y + 1]).WaterLevel < MAX_WATER_LEVEL)
{
if (((FluidTile)tg.World[x, y]).WaterLevel >= MAX_WATER_LEVEL - ((FluidTile)tg.World[x, y + 1]).WaterLevel)
{
((FluidTile)tg.World[x, y]).WaterLevel -= (byte)(MAX_WATER_LEVEL - ((FluidTile)tg.World[x, y + 1]).WaterLevel);
((FluidTile)tg.World[x, y + 1]).WaterLevel = MAX_WATER_LEVEL;
if (((FluidTile)tg.World[x, y]).WaterLevel == 0)
{
tg.World[x, y] = new Tile(TileType.None, tg.World[x, y].BackgroundType);
continue;
}
}
else
{
((FluidTile)tg.World[x, y + 1]).WaterLevel += ((FluidTile)tg.World[x, y]).WaterLevel;
tg.World[x, y] = new Tile(TileType.None, tg.World[x, y].BackgroundType);
continue;
}
}
}
else if (tg.World[x, y + 1].Collision != TileCollision.Impassable)
{
tg.World[x, y + 1] = new FluidTile(TileType.Water, tg.World[x, y + 1].BackgroundType, ((FluidTile)tg.World[x, y]).WaterLevel);
tg.World[x, y] = new Tile(TileType.None, tg.World[x, y].BackgroundType);
continue;
}
//Water flows leftwards
byte onefourth = (byte)(((FluidTile)tg.World[x, y]).WaterLevel / 4);

if (tg.World[x - 1, y].Type == TileType.Water)
{
if (((FluidTile)tg.World[x - 1, y]).WaterLevel + onefourth <= MAX_WATER_LEVEL)
{
((FluidTile)tg.World[x - 1, y]).WaterLevel += onefourth;
((FluidTile)tg.World[x, y]).WaterLevel -= onefourth;
}
}
else if (tg.World[x - 1, y].Collision != TileCollision.Impassable)
{
tg.World[x - 1, y] = new FluidTile(TileType.Water, tg.World[x - 1, y].BackgroundType, onefourth);
((FluidTile)tg.World[x, y]).WaterLevel -= onefourth;
}
//Water flows Rightwards
if (tg.World[x + 1, y].Type == TileType.Water)
{
if (((FluidTile)tg.World[x + 1, y]).WaterLevel + onefourth <= MAX_WATER_LEVEL)
{
((FluidTile)tg.World[x + 1, y]).WaterLevel += onefourth;
((FluidTile)tg.World[x, y]).WaterLevel -= onefourth;
}
}
else if (tg.World[x + 1, y].Collision != TileCollision.Impassable)
{
tg.World[x + 1, y] = new FluidTile(TileType.Water, tg.World[x + 1, y].BackgroundType, onefourth);
((FluidTile)tg.World[x, y]).WaterLevel -= onefourth;
}
}
}
}
[/CODE]

The code in the top that is commented out is partly for controlling speed, but also because I feared performance issues, so I have it in place to limit the amount of cycles pr second used, if needed.

So my questions are:
- Did I stare myself blind on this and did I make an error I failed to find?
- If not- What changes to this method would possibly improve this?

If I am going about this the wrong way, don't hesitate to point it out- This game is supposed to be a learning experience after all [img]http://public.gamedev.net//public/style_emoticons/default/smile.png[/img]

Thank you for reading [img]http://public.gamedev.net//public/style_emoticons/default/smile.png[/img]
0

Share this post


Link to post
Share on other sites
[CODE]
private void UpdateWater(GameTime gameTime)
{
/*_waterTimer -= (float)gameTime.ElapsedGameTime.TotalMilliseconds;
if (_waterTimer > 0)
return;
_waterTimer = 34;*/
for (int x = (int)(_pc.Position.X / StaticTileInfo.Size.X) - UPDATE_RADIUS_X; x < (int)(_pc.Position.X / StaticTileInfo.Size.X) + UPDATE_RADIUS_X; x++)
{
for (int y = (int)(_pc.Position.Y / StaticTileInfo.Size.Y) + UPDATE_RADIUS_Y; y > (int)(_pc.Position.Y / StaticTileInfo.Size.Y) - UPDATE_RADIUS_Y; y--)
{
if (x < 0 || x > tg.World.GetLength(0) || y < 0 || y > tg.World.GetLength(1))
continue;
if (tg.World[x, y].Type != TileType.Water)
continue;
//Water flows downwards
if (tg.World[x, y + 1].Type == TileType.Water)
{
if (((FluidTile)tg.World[x, y + 1]).WaterLevel < MAX_WATER_LEVEL)
{
if (((FluidTile)tg.World[x, y]).WaterLevel >= MAX_WATER_LEVEL - ((FluidTile)tg.World[x, y + 1]).WaterLevel)
{
((FluidTile)tg.World[x, y]).WaterLevel -= (byte)(MAX_WATER_LEVEL - ((FluidTile)tg.World[x, y + 1]).WaterLevel);
((FluidTile)tg.World[x, y + 1]).WaterLevel = MAX_WATER_LEVEL;
if (((FluidTile)tg.World[x, y]).WaterLevel == 0)
{
tg.World[x, y] = new Tile(TileType.None, tg.World[x, y].BackgroundType);
continue;
}
}
else
{
((FluidTile)tg.World[x, y + 1]).WaterLevel += ((FluidTile)tg.World[x, y]).WaterLevel;
tg.World[x, y] = new Tile(TileType.None, tg.World[x, y].BackgroundType);
continue;
}
}
}
else if (tg.World[x, y + 1].Collision != TileCollision.Impassable)
{
tg.World[x, y + 1] = new FluidTile(TileType.Water, tg.World[x, y + 1].BackgroundType, ((FluidTile)tg.World[x, y]).WaterLevel);
tg.World[x, y] = new Tile(TileType.None, tg.World[x, y].BackgroundType);
continue;
}
//Water flows leftwards
byte onefourth = (byte)(((FluidTile)tg.World[x, y]).WaterLevel / 4);

if (tg.World[x - 1, y].Type == TileType.Water)
{
if (((FluidTile)tg.World[x - 1, y]).WaterLevel + onefourth <= MAX_WATER_LEVEL)
{
((FluidTile)tg.World[x - 1, y]).WaterLevel += onefourth;
((FluidTile)tg.World[x, y]).WaterLevel -= onefourth;
}
}
else if (tg.World[x - 1, y].Collision != TileCollision.Impassable)
{
tg.World[x - 1, y] = new FluidTile(TileType.Water, tg.World[x - 1, y].BackgroundType, onefourth);
((FluidTile)tg.World[x, y]).WaterLevel -= onefourth;
}
//Water flows Rightwards
if (tg.World[x + 1, y].Type == TileType.Water)
{
if (((FluidTile)tg.World[x + 1, y]).WaterLevel + onefourth <= MAX_WATER_LEVEL)
{
((FluidTile)tg.World[x + 1, y]).WaterLevel += onefourth;
((FluidTile)tg.World[x, y]).WaterLevel -= onefourth;
}
}
else if (tg.World[x + 1, y].Collision != TileCollision.Impassable)
{
tg.World[x + 1, y] = new FluidTile(TileType.Water, tg.World[x + 1, y].BackgroundType, onefourth);
((FluidTile)tg.World[x, y]).WaterLevel -= onefourth;
}
}
}
}
[/CODE]
Fixed.


L. Spiro
1

Share this post


Link to post
Share on other sites
From the description of the results you get, the description of the algorithm you wanted to implement, and a very quick look at the code, there is a logical error that you should handle.
Whether or not it gives you correct results is yet to be seen, but it explains why the right buckets have less than the left sides.

That is because you handle filling to the left before filling to the right.
When you are spilling down from a higher bucket into a lower one, there should be an extra check for both the left and right sides needing to be filled.
Currently you check the left, fill it, then check the right. By that time it is possible that there is not enough water to spill over to the right because you used it all on the left.

Before checking for left spillage, add a check for both right and left spillage together, then spill by ? in both directions instead of ¼.


L. Spiro
1

Share this post


Link to post
Share on other sites
[quote name='L. Spiro' timestamp='1351024603' post='4993199']
From the description of the results you get, the description of the algorithm you wanted to implement, and a very quick look at the code, there is a logical error that you should handle.
Whether or not it gives you correct results is yet to be seen, but it explains why the right buckets have less than the left sides.

That is because you handle filling to the left before filling to the right.
When you are spilling down from a higher bucket into a lower one, there should be an extra check for both the left and right sides needing to be filled.
Currently you check the left, fill it, then check the right. By that time it is possible that there is not enough water to spill over to the right because you used it all on the left.

Before checking for left spillage, add a check for both right and left spillage together, then spill by ? in both directions instead of ¼.


L. Spiro
[/quote]

Thanks a lot for the quick response. It makes sense what you say. I won't be able to try it out until tomorrow night (it's over 11pm here). But it does make sense to check in both direction.. But I'm thinking I define ¼. before doing any of the checks, based on the current remaining water. Shouldn't there be enough no matter what? Since 2 * ¼. = ½ of the remaining water. I might be wrong, a bit tired now :)

But I will apply your logic no matter what, since it seems that it favors one side from my tests there must be truth in what you say :D

Again- Thanks a lot for a quick and good response.
0

Share this post


Link to post
Share on other sites
[quote name='Mekuri' timestamp='1351026482' post='4993206']
I'm thinking I define &frac14;. before doing any of the checks, based on the current remaining water. Shouldn't there be enough no matter what? Since 2 * &frac14;. = &frac12; of the remaining water.[/quote]
That is technically correct and may even be natural (you could say the water is draining twice as fast because it has twice as many places to go) but it would be worth an investigation to see how it looks when adding only half to each direction.


There is something else that [i]might[/i] need to be fixed.
Using only the left code in my example (this applies to both sides), you do this:
[CODE] if (((FluidTile)tg.World[x - 1, y]).WaterLevel + onefourth <= MAX_WATER_LEVEL)
{
((FluidTile)tg.World[x - 1, y]).WaterLevel += onefourth;
((FluidTile)tg.World[x, y]).WaterLevel -= onefourth;
}[/CODE]

Due to clause #1 in which you move as much water from the top cell down to the bottom cell as you can, there may be the possibility that water is not distributed in even ¼’s to each cell.
This depends on unknown facts about the environment in which your cells/buckets live, so I can’t say for sure if this is a problem, but if there is any possibility that buckets are increased by anything other than even multiples of ¼, the code above needs to be fixed.
In that case you need to add as much to the bucket as will fit and add the rest to the bucket above it.
If you can guarantee your system always transfers multiples of ¼ each time, this is not a problem.


L. Spiro
1

Share this post


Link to post
Share on other sites
Hey there.
So I had a few hours to continue with this problem, and I've made some huge changes to how I handle water in my world. Instead of having a specific tile type for water, I instead added a water variable to all tiles. This allows me to flood things, instead of removing them (because the water tile would take the place of the... tree tile for example)This also simplified the code a bit, since I no longer need to check if the neighbouring tile is of the right type or not, I merely check if it contains any water or not, so no more typecasting back and forth [img]http://public.gamedev.net//public/style_emoticons/default/smile.png[/img]
So this was the good news, I like it when there are positive side effects of problem solving...
Now, although I've been able to simplify my code, I don't feel any closer to a solution.
I tried the solution where I check both sides before pouring water in to them, but it gave the same results. I also tried to make sure that whenever water transfered downwards it was divideable with four. I might have messed something up by trying that, because I got very unpredictable results, but I was unable to see where, if so.
Anyways, now I have two code versions. The one is basically the one I posted earlier, a bit more simplified, which I will post below.
The other version I've made it so that a tile can contain no more than 16 units of water, and every time I transfer left or right, I only transfer 1 unit. This seems to be a very fast method, but the results are... crazy. So I decided to make a small video demonstrating both approaches, but first I'll post the code for them both

[b]Code for 16 units max and 1 unit pr transfer:[/b]
[CODE]
for (int x = (int)(_pc.Position.X / StaticTileInfo.Size.X) - UPDATE_RADIUS_X; x < (int)(_pc.Position.X / StaticTileInfo.Size.X) + UPDATE_RADIUS_X; x++)
{
for (int y = (int)(_pc.Position.Y / StaticTileInfo.Size.Y) + UPDATE_RADIUS_Y; y > (int)(_pc.Position.Y / StaticTileInfo.Size.Y) - UPDATE_RADIUS_Y; y--)
{
if (x < 0 || x > tg.World.GetLength(0) || y < 0 || y > tg.World.GetLength(1))
continue;
if (tg.World[x, y].WaterLevel == 0)
continue;
//Water flows downwards
if (tg.World[x, y + 1].WaterLevel < MAX_WATER_LEVEL && tg.World[x, y + 1].Collision != TileCollision.Impassable)
{
if (tg.World[x, y].WaterLevel >= MAX_WATER_LEVEL - tg.World[x, y + 1].WaterLevel)
{
tg.World[x, y].WaterLevel -= (byte)(MAX_WATER_LEVEL - tg.World[x, y + 1].WaterLevel);
tg.World[x, y + 1].WaterLevel = MAX_WATER_LEVEL;

if (tg.World[x, y].WaterLevel == 0)
continue;
}
else
{
tg.World[x, y + 1].WaterLevel += tg.World[x, y].WaterLevel;
tg.World[x, y].WaterLevel = 0;
continue;
}
}

//Water flows leftwards
//byte onefourth = (byte)(tg.World[x, y].WaterLevel / 4);
byte onefourth = 1;
if (tg.World[x - 1, y].WaterLevel + onefourth <= MAX_WATER_LEVEL && tg.World[x - 1, y].Collision != TileCollision.Impassable)
{
tg.World[x - 1, y].WaterLevel += onefourth;
tg.World[x, y].WaterLevel -= onefourth;
}

//Water flows Rightwards
if (tg.World[x + 1, y].WaterLevel + onefourth <= MAX_WATER_LEVEL && tg.World[x + 1, y].Collision != TileCollision.Impassable)
{
tg.World[x + 1, y].WaterLevel += onefourth;
tg.World[x, y].WaterLevel -= onefourth;
}
}
}
}

[/CODE]

[b]The "original code" - Max waterlevel = 100, and transfers 1/4 left and right.[/b]
[CODE]
for (int x = (int)(_pc.Position.X / StaticTileInfo.Size.X) - UPDATE_RADIUS_X; x < (int)(_pc.Position.X / StaticTileInfo.Size.X) + UPDATE_RADIUS_X; x++)
{
for (int y = (int)(_pc.Position.Y / StaticTileInfo.Size.Y) + UPDATE_RADIUS_Y; y > (int)(_pc.Position.Y / StaticTileInfo.Size.Y) - UPDATE_RADIUS_Y; y--)
{
if (x < 0 || x > tg.World.GetLength(0) || y < 0 || y > tg.World.GetLength(1))
continue;
if (tg.World[x, y].WaterLevel == 0)
continue;
//Water flows downwards
if (tg.World[x, y + 1].WaterLevel < MAX_WATER_LEVEL && tg.World[x, y + 1].Collision != TileCollision.Impassable)
{
if (tg.World[x, y].WaterLevel >= MAX_WATER_LEVEL - tg.World[x, y + 1].WaterLevel)
{
tg.World[x, y].WaterLevel -= (byte)(MAX_WATER_LEVEL - tg.World[x, y + 1].WaterLevel);
tg.World[x, y + 1].WaterLevel = MAX_WATER_LEVEL;

if (tg.World[x, y].WaterLevel == 0)
continue;
}
else
{
tg.World[x, y + 1].WaterLevel += tg.World[x, y].WaterLevel;
tg.World[x, y].WaterLevel = 0;
continue;
}
}
//Water flows leftwards
byte onefourth = (byte)(tg.World[x, y].WaterLevel / 4);[/b]
if (tg.World[x - 1, y].WaterLevel + onefourth <= MAX_WATER_LEVEL && tg.World[x - 1, y].Collision != TileCollision.Impassable)
{
tg.World[x - 1, y].WaterLevel += onefourth;
tg.World[x, y].WaterLevel -= onefourth;
}
else if (tg.World[x - 1, y].WaterLevel + (byte)(onefourth / 2) <= MAX_WATER_LEVEL && tg.World[x - 1, y].Collision != TileCollision.Impassable)
{
tg.World[x - 1, y].WaterLevel += (byte)(onefourth / 2);
tg.World[x, y].WaterLevel -= (byte)(onefourth / 2);
}
//Water flows Rightwards
if (tg.World[x + 1, y].WaterLevel + onefourth <= MAX_WATER_LEVEL && tg.World[x + 1, y].Collision != TileCollision.Impassable)
{
tg.World[x + 1, y].WaterLevel += onefourth;
tg.World[x, y].WaterLevel -= onefourth;
}
else if (tg.World[x + 1, y].WaterLevel + (byte)(onefourth / 2) <= MAX_WATER_LEVEL && tg.World[x + 1, y].Collision != TileCollision.Impassable)
{
tg.World[x + 1, y].WaterLevel += (byte)(onefourth / 2);
tg.World[x, y].WaterLevel -= (byte)(onefourth / 2);
}
}
}
[/CODE]

And here's the video:

[media]http://youtu.be/EZ34EOnNID8[/media]

I'll throw in a few annotations in the video to help guide the attention. It's just a quick video with very little editing.

I really appreciate any time spend helping me out [img]http://public.gamedev.net//public/style_emoticons/default/smile.png[/img] I feel I'm soo close to getting water off my to do list [img]http://public.gamedev.net//public/style_emoticons/default/smile.png[/img] Thanks
0

Share this post


Link to post
Share on other sites
Take the update order into account. For each tile, BOTH push and pull water to balance the levels. Else the update order affects how the water moves.

You should probably divide the water-flow-speed by 2 because for each link between 2 tiles, there might be 1 pull and 1 push if it doesnt balance out in the first tile update.
1

Share this post


Link to post
Share on other sites
[quote name='Waterlimon' timestamp='1351086889' post='4993426']
Take the update order into account. For each tile, BOTH push and pull water to balance the levels. Else the update order affects how the water moves.

You should probably divide the water-flow-speed by 2 because for each link between 2 tiles, there might be 1 pull and 1 push if it doesnt balance out in the first tile update.
[/quote]
If I understood you correctly each time I push water to the other tiles, I then try to pull some back, in case the push caused the receiveing tile to become more filled than the pushing tile. Is this correct?
Assuming it is I will try this solution, thanks you [img]http://public.gamedev.net//public/style_emoticons/default/smile.png[/img]

Edit:
Or do you mean I should run through all the water twice, one forward and one backwards? Each taking their turn, so 30 updates for left to right, and 30 for right to left? - Left, right, left, right etc.? Edited by Mekuri
0

Share this post


Link to post
Share on other sites
no, first move water down.

then on another run, for both sides of a water, if the side is higher water level you pull water to the middle, else you push water to it.

so each water pushes and/or pulls water from both sides depending on whether they have more or less water.

And when pushing or pulling make sure you dont overfill a water block
1

Share this post


Link to post
Share on other sites
[quote name='Waterlimon' timestamp='1351094547' post='4993464']
no, first move water down.

then on another run, for both sides of a water, if the side is higher water level you pull water to the middle, else you push water to it.

so each water pushes and/or pulls water from both sides depending on whether they have more or less water.

And when pushing or pulling make sure you dont overfill a water block
[/quote]

Hey there. Thanks for the advice. I've now tried what you suggested but the results aren't that good. I get this.. wobbly surface, as if small regions are pushing water back and forth between each other.
I've tried the following:
- One run where it only updates the water downwards, followed by another run, doing pushes and pulls both left and right for each tile.
- Two runs, where each run both updates water downwards and pushes and pulls in both direction. One run updating towards left, and the other towards right.
- Two runs, no pulling only pushing. Each update both updates downwards and left and right. One updating from left to right, and the other right to left.

The best result is the last method, but that doesn't necesarily makes it the most correct one, since none of them is doing it right.
My current code is based on your suggestion, so please do tell me if I misunderstood that as well [img]http://public.gamedev.net//public/style_emoticons/default/tongue.png[/img]

[CODE]
private void UpdateWater(GameTime gameTime)
{
if (!_updateWaterDown)
{
for (int x = (int)(_pc.Position.X / StaticTileInfo.Size.X) - UPDATE_RADIUS_X; x < (int)(_pc.Position.X / StaticTileInfo.Size.X) + UPDATE_RADIUS_X; x++)
{
for (int y = (int)(_pc.Position.Y / StaticTileInfo.Size.Y) + UPDATE_RADIUS_Y; y > (int)(_pc.Position.Y / StaticTileInfo.Size.Y) - UPDATE_RADIUS_Y; y--)
{
if (x < 0 || x > tg.World.GetLength(0) || y < 0 || y > tg.World.GetLength(1))
continue;
if (tg.World[x, y].WaterLevel == 0)
continue;
//Water flows to/from left
byte onefourth = (byte)(tg.World[x, y].WaterLevel / 4);
if (onefourth == 0)
onefourth = 1;
if (tg.World[x, y].WaterLevel > tg.World[x - 1, y].WaterLevel)
{
if (tg.World[x - 1, y].WaterLevel + onefourth <= MAX_WATER_LEVEL && tg.World[x - 1, y].Collision != TileCollision.Impassable)
{
tg.World[x - 1, y].WaterLevel += onefourth;
tg.World[x, y].WaterLevel -= onefourth;
}
else if (tg.World[x - 1, y].WaterLevel + (byte)(onefourth / 2) <= MAX_WATER_LEVEL && tg.World[x - 1, y].Collision != TileCollision.Impassable)
{
tg.World[x - 1, y].WaterLevel += (byte)(onefourth / 2);
tg.World[x, y].WaterLevel -= (byte)(onefourth / 2);
}
}
else if (tg.World[x, y].WaterLevel < tg.World[x - 1, y].WaterLevel)
{
if (tg.World[x, y].WaterLevel + onefourth <= MAX_WATER_LEVEL)
{
tg.World[x, y].WaterLevel += onefourth;
tg.World[x - 1, y].WaterLevel -= onefourth;
}
else if (tg.World[x, y].WaterLevel + (byte)(onefourth / 2) <= MAX_WATER_LEVEL)
{
tg.World[x, y].WaterLevel += (byte)(onefourth / 2);
tg.World[x - 1, y].WaterLevel -= (byte)(onefourth / 2);
}
}
//Water flows to/from right
if (tg.World[x, y].WaterLevel > tg.World[x + 1, y].WaterLevel)
{
if (tg.World[x + 1, y].WaterLevel + onefourth <= MAX_WATER_LEVEL && tg.World[x + 1, y].Collision != TileCollision.Impassable)
{
tg.World[x + 1, y].WaterLevel += onefourth;
tg.World[x, y].WaterLevel -= onefourth;
}
else if (tg.World[x + 1, y].WaterLevel + (byte)(onefourth / 2) <= MAX_WATER_LEVEL && tg.World[x + 1, y].Collision != TileCollision.Impassable)
{
tg.World[x + 1, y].WaterLevel += (byte)(onefourth / 2);
tg.World[x, y].WaterLevel -= (byte)(onefourth / 2);
}
}
else if (tg.World[x, y].WaterLevel < tg.World[x + 1, y].WaterLevel)
{
if (tg.World[x, y].WaterLevel + onefourth <= MAX_WATER_LEVEL)
{
tg.World[x, y].WaterLevel += onefourth;
tg.World[x + 1, y].WaterLevel -= onefourth;
}
else if (tg.World[x, y].WaterLevel + (byte)(onefourth / 2) <= MAX_WATER_LEVEL)
{
tg.World[x, y].WaterLevel += (byte)(onefourth / 2);
tg.World[x + 1, y].WaterLevel -= (byte)(onefourth / 2);
}
}
}
}
_updateWaterDown = !_updateWaterDown;
}
else
{
for (int x = (int)(_pc.Position.X / StaticTileInfo.Size.X) + UPDATE_RADIUS_X; x > (int)(_pc.Position.X / StaticTileInfo.Size.X) - UPDATE_RADIUS_X; x--)
{
for (int y = (int)(_pc.Position.Y / StaticTileInfo.Size.Y) + UPDATE_RADIUS_Y; y > (int)(_pc.Position.Y / StaticTileInfo.Size.Y) - UPDATE_RADIUS_Y; y--)
{
if (x < 0 || x > tg.World.GetLength(0) || y < 0 || y > tg.World.GetLength(1))
continue;
if (tg.World[x, y].WaterLevel == 0)
continue;
//Water flows downwards
if (tg.World[x, y + 1].WaterLevel < MAX_WATER_LEVEL && tg.World[x, y + 1].Collision != TileCollision.Impassable)
{
if (tg.World[x, y].WaterLevel >= MAX_WATER_LEVEL - tg.World[x, y + 1].WaterLevel)
{
tg.World[x, y].WaterLevel -= (byte)(MAX_WATER_LEVEL - tg.World[x, y + 1].WaterLevel);
tg.World[x, y + 1].WaterLevel = MAX_WATER_LEVEL;
if (tg.World[x, y].WaterLevel == 0)
continue;
}
else
{
tg.World[x, y + 1].WaterLevel += tg.World[x, y].WaterLevel;
tg.World[x, y].WaterLevel = 0;
continue;
}
}
}
}
_updateWaterDown = !_updateWaterDown;
}
}
[/CODE]

Am I doing it the right way?
Is there another approach I should consider?

This is a tough one, and I've failed to dig up much information about tile based water online. I really appreciate all the help I get here, and any suggestions are welcome.

Thank you [img]http://public.gamedev.net//public/style_emoticons/default/smile.png[/img]
0

Share this post


Link to post
Share on other sites
If its a flat surface, there will be pieces moving around as long as you use integers or similiar for water level.

The moving can get fixed by writing to a different grid than you read from (so it doesnt change while updating)

But if you want a smooth surface you need to use floats for water level OR not display water amounts equal to the minimum water amount.


So, i suggest making your cide not render blocks with exactly 1 unit of water, which hides the moving water pieces so there will be no moving thingies on top AND no thingies on top (of course :P)
1

Share this post


Link to post
Share on other sites
But if you want a smooth surface you need to use floats for water level OR not display water amounts equal to the minimum water amount.


Hey again. So I tried implementing your suggestions, by not drawing tiles with the minimum (or close to) amount of water.. it helped, but it still wasn't there. I also tried changing all the bytes to floats (which gave me a close to 20mb size increase on my large world save files tongue.png ) this made the process much smoother, but I still ended up with uneven surfaces. I am sure if I continued I would've found a fitting solution based on your ideas...

Last night however, I found this article: http://w-shadow.com/...uid-simulation/ - Basically you create water using the Cellular Automata method. I don't fully understand the whole concept of Cellular Automata, but at least for my needs his explanations are good enough.
It was actually your suggestion about using floats, that made me decide to go with this method.
I read the article a few times, until I had a better understanding of what he was doing- afterwards I went down to the coding, basically following his code and changing it to my needs. The results are... decent.
The water now evens out eventually, but as with the other approach, this also takes some time, albeit a bit faster. This method also includes presure, at least a little bit. For example, if we have a U shaped pipe and I pour water into the left side of the pipe there water would, given enough water, start to fill up the right end of the pipe.

I am hoping in the future that I will be able to make this somewhat faster but for now it suits my needs. I will post a video in this thread when I get the graphics sorted proberly, showing what this method allows, and also the downsides of it.

If anyone wants to see the code (at least the current code) here it is:

 

		private const float MAX_WATER_LEVEL = 1.0f;
		private const float MAX_COMPRESS = 0.02f;
		private const float MIN_WATER_LEVEL = 0.0001f;
		private const float MIN_FLOW = 0.01f;
		private const float MAX_SPEED = 1f;
		private void UpdateWater(GameTime gameTime)
		{
			float flow = 0;
			float remaining_mass;
			for (int x = (int)(_pc.Position.X / StaticTileInfo.Size.X) - UPDATE_RADIUS_X; x < (int)(_pc.Position.X / StaticTileInfo.Size.X) + UPDATE_RADIUS_X; x++)
			{
				for (int y = (int)(_pc.Position.Y / StaticTileInfo.Size.Y) - UPDATE_RADIUS_Y; y < (int)(_pc.Position.Y / StaticTileInfo.Size.Y) + UPDATE_RADIUS_Y; y++)
				{
					if (x - 1 < 0 || y - 1 < 0 || x + 1 > tg.World.GetLength(0) || y + 1 > tg.World.GetLength(1))
						continue;
					//Ignore blocks with little to no water.
					if (tg.World[x, y].Collision == TileCollision.Impassable || tg.World[x,y].WaterLevel < MIN_WATER_LEVEL)
						continue;
					//Push flow
					flow = 0;
					remaining_mass = tg.World[x, y].WaterLevel;
					if (remaining_mass <= 0)
						continue;
				  
					//The tile below this one
					if (tg.World[x, y + 1].Collision != TileCollision.Impassable)
					{
						flow = GetVerticalAdjacentMaxWater(remaining_mass + tg.World[x, y + 1].WaterLevel) - tg.World[x, y + 1].WaterLevel;
						if(flow > MIN_FLOW)
							flow *= .5f; //Smooths the flow
						flow = MathHelper.Clamp(flow, 0, MathHelper.Min(MAX_SPEED, remaining_mass));
						tg.World[x, y].WaterLevel -= flow;
						tg.World[x, y + 1].WaterLevel += flow;
						remaining_mass -= flow;
					}
					if (remaining_mass <= 0)
						continue;
					//Left
					if (tg.World[x - 1, y].Collision != TileCollision.Impassable)
					{
						//Equalize amount of water in this block and the neighbour
						flow = (tg.World[x, y].WaterLevel - tg.World[x - 1, y].WaterLevel) / 4;
						if (flow > MIN_FLOW)
							flow *= 0.5f;
						flow = MathHelper.Clamp(flow, 0, remaining_mass);
						tg.World[x, y].WaterLevel -= flow;
						tg.World[x - 1, y].WaterLevel += flow;
						remaining_mass -= flow;
					}
					if (remaining_mass <= 0)
						continue;
					//Right
					if (tg.World[x + 1, y].Collision != TileCollision.Impassable)
					{
						//Equalize amount of water in this block and the neighbour
						flow = (tg.World[x, y].WaterLevel - tg.World[x + 1, y].WaterLevel) / 4;
						if (flow > MIN_FLOW)
							flow *= 0.5f;
						flow = MathHelper.Clamp(flow, 0, remaining_mass);
						tg.World[x, y].WaterLevel -= flow;
						tg.World[x + 1, y].WaterLevel += flow;
						remaining_mass -= flow;
					}
					if (remaining_mass <= 0)
						continue;
					//Up - Only compressed water tiles flow upwards.
					if (tg.World[x, y - 1].Collision != TileCollision.Impassable)
					{
						flow = remaining_mass - GetVerticalAdjacentMaxWater(remaining_mass + tg.World[x, y - 1].WaterLevel);
						if (flow > MIN_FLOW)
							flow *= 0.5f;
						flow = MathHelper.Clamp(flow, 0, MathHelper.Min(MAX_SPEED, remaining_mass));
						tg.World[x, y].WaterLevel -= flow;
						tg.World[x, y - 1].WaterLevel += flow;
						remaining_mass -= flow;
					}
				}
			}
		}
		private float GetVerticalAdjacentMaxWater(float total_mass)
		{
			if (total_mass <= 1)
				return 1;
			else if (total_mass < 2 * MAX_WATER_LEVEL + MAX_COMPRESS)
				return (MAX_WATER_LEVEL * MAX_WATER_LEVEL + total_mass * MAX_COMPRESS) / (MAX_WATER_LEVEL + MAX_COMPRESS);
			else
				return (total_mass + MAX_COMPRESS) / 2;
		}


Keep in mind this code is specifically made for my game, but I believe that it should be fairly easy to understand what to change to make it suit your needs. Notice that in my game minus on the Y axis is up, plus is down. In the article I linked, this is the other way around.
I thank you guys for all the help, and I hope that my code here might help someone in the right direction sometime. If anyone has ideas for speed optimizations, please do post them, and I'll update the code accordingly.

Thank you for reading smile.png

Edited by Mekuri
0

Share this post


Link to post
Share on other sites
As I promised I'd throw the video with my results here. There's still some tweaking to do, especially with the drawing of the water part.. And some optimization on the horisontal spread would definitely improve this.

Anyways I just wanted to thank you guys for your help again, and post the video.

If you're not interested in the other stuff in the video just fastforward to around 0:55.

[media]http://youtu.be/GSQaHqybhjM[/media]

Enjoy!
0

Share this post


Link to post
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!


Register a new account

Sign in

Already have an account? Sign in here.


Sign In Now
Sign in to follow this  
Followers 0