6

Demonstration of strange water physics

I am currently working on a very simple 'Falling Sand' simulation game in C++ and SDL2, and am having problems with getting water to flow in a more realistic manner. I basically have a grid of cells that I iterate through bottom-to-top, left-to-right and if I find a water cell, I just check below, down to left, down to the right, left then right for empty cells and it moves into the first one its finds (it makes a random choice if both diagonal cells or both horizontal cells are free). I then mark the cell it moved into as processed so that it is not checked again for the rest of that loop.

My problem is a sort of 'left-bias' in how the particles move; if I spawn a square of water cells above a barrier, they will basically all shift to left without moving once the particles begin to reach the barrier, while the cells on the right will run down in the proper way. So instead of forming a nice triangular shape flowing out evenly to both sides, the whole shape will just move to the left. This effect is reversed whenever I iterate left-to-right, so I know it's something to do with that but so far I've been stumped trying to fix it. I initially thought it was a problem with how I marked the cells as processed but I've found no obvious bugs with that system in many hours of testing. Has anyone faced any similar challeneges in developing a simulation like this, or knows something that I'm missing? Any help would be very much appreciated.

EDIT: Ok so I've made a little progress, however I've ran into another bug that seems to be unrelated to iteration, since now I save a copy of the old cells and read from that to decide an update, then update the original cells and display that. This already made the sand work better, however water, which checks horizontally for free cells, now 'disappears' when it does move horizontally. I've been testing it all morning and have yet to find a solution, I thought it might've been someting to do with how I was copying the arrays over, but it seems to work as far as I can tell.

New snippets:

Simulation.cpp

void Simulation::update()
{
    copyStates(m_cells, m_oldCells); // so now oldcells is the last new state

    for(int y = m_height - 1; y>= 0; y--)
    for(int x = 0; x < m_width; x++)
        {
            Cell* c = getOldCell(x, y); // check in the old state for possible updates
            switch(c->m_type)
            {
                case EMPTY:
                    break;
                case SAND:
                    if(c->m_visited == false) update_sand(x, y);
                    break;
                case WATER:
                    if(c->m_visited == false) update_water(x, y);
                    break;
                default:
                    break;
            }
        }
}

void Simulation::update_water(int x, int y)
{
    bool down = (getOldCell(x, y+1)->m_type == EMPTY) && checkBounds(x, y+1) && !getOldCell(x, y+1)->m_visited;
    bool d_left = (getOldCell(x-1, y+1)->m_type == EMPTY) && checkBounds(x-1, y+1) && !getOldCell(x-1, y+1)->m_visited;
    bool d_right = (getOldCell(x+1, y+1)->m_type == EMPTY) && checkBounds(x+1, y+1) && !getOldCell(x+1, y+1)->m_visited ;
    bool left = (getOldCell(x-1, y)->m_type == EMPTY) && checkBounds(x-1, y) && !getOldCell(x-1, y)->m_visited ;
    bool right = (getOldCell(x+1, y)->m_type == EMPTY) && checkBounds(x+1, y) && !getOldCell(x+1, y)->m_visited ;

    // choose random dir if both are possible
    if(d_left && d_right)
    {
        int r = rand() % 2;
        if(r) d_right = false;
        else d_left = false;
    }

    if(left && right)
    {
        int r = rand() % 2;
        if(r) right = false;
        else left = false;
    }
    
    if(down)
    {
        getCell(x, y+1)->m_type = WATER; // we now update the new state
        getOldCell(x, y+1)->m_visited = true; // mark as visited so it will not be checked again in update()
    } else if(d_left)
    {
        getCell(x-1, y+1)->m_type = WATER;
        getOldCell(x-1, y+1)->m_visited = true;
    } else if(d_right)
    {
        getCell(x+1, y+1)->m_type = WATER;
        getOldCell(x+1, y+1)->m_visited = true;
    } else if(left)
    {
        getCell(x-1, y)->m_type = WATER;
        getOldCell(x-1, y)->m_visited = true;
    } else if(right)
    {
        getCell(x+1, y)->m_type = WATER;
        getOldCell(x+1, y)->m_visited = true;
    }
    
    if(down || d_right || d_left || left || right) // the original cell is now empty; update the new state
    {
        getCell(x, y)->m_type = EMPTY;
    }
}

void Simulation::copyStates(Cell* from, Cell* to)
{
    for(int x = 0; x < m_width; x++)
    for(int y = 0; y < m_height; y++)
    {
        to[x + y * m_width].m_type = from[x + y * m_width].m_type;
        to[x + y * m_width].m_visited = from[x + y * m_width].m_visited;
    }
}

Main.cpp

sim.update();

Uint32 c_sand = 0xedec9a00;
for(int y = 0; y < sim.m_height; y++)
for(int x = 0; x < sim.m_width; x++)
{
    sim.getCell(x, y)->m_visited = false;
    if(sim.getCell(x, y)->m_type == 0) screen.setPixel(x, y, 0);
    if(sim.getCell(x, y)->m_type == 1) screen.setPixel(x, y, c_sand);
    if(sim.getCell(x, y)->m_type == 2) screen.setPixel(x, y, 0x0000cc00);
}


screen.render();

I've attached a gif showing the problem, hopefully this might help make it a little clearer. You can see the sand being placed normally, then the water and the strange patterns it makes after being placed (notice how it moves off to the left when it's spawned, unlike the sand)

James Mclaughlin
  • 570
  • 1
  • 8
  • 16
  • 1
    Rather than process left-to-right, work from the outside to the inside. We can only speculate without seeing your code. [Edit] the question to include a [mre]. – 1201ProgramAlarm Mar 08 '21 at 00:50
  • @1201ProgramAlarm Ok, I've actually added a new system that makes a separate copy of the cells, checks an old one for changes, then updates the new one accoridingly, which did totally remove the weird effect for the sand particles. However it's introduced a new bug for the water particles, which now dissappear when they come into contact with the barrier/sand. Thanks for the suggestion though. – James Mclaughlin Mar 08 '21 at 13:13
  • 1
    Think about what happens in that last if of `update_water` when you set the original cell to EMPTY. Is it overwriting a previous change? – 1201ProgramAlarm Mar 08 '21 at 19:43
  • I'm really not sure about this now. I added an extra directional check to see if the old_cell had been visited which I realised I had not been checking, but this has again introduced an obvious directional bias, but it did stop the water from disappearing. In reference to your question, I'm not entirely sure but I don't think it would be possible for me to overwrite a change since I go through each cell only once, or am I missing something? – James Mclaughlin Mar 08 '21 at 23:04
  • Create some unit tests with very simple setups of only a few pixels set. And perhaps you can make the random number generator deterministic for those tests or let it run multiple times and get a statistic of different results. – Sebastian Jan 01 '22 at 09:32

1 Answers1

0

You also have to mark the destination postion as visited to stop multiple cells moving in to the same place.

Joe Eaves
  • 387
  • 3
  • 3