1

I have been trying to implement 2D tile based water into my game. I started out making the tiles appear on screen etc. I have a drawing function that draws each tile type. The problem I'm having is that when I call this function the Tiles that are water don't change position. Which makes me believe that this code isn't functioning properly. This code is called on every loop. This should update the masses of all the water tiles. For some reason nothing is happening. The water is staying in its original positions. My tiles are in a vector of the tile class is just

Tiles()
{
    TileProp // the type of tile (GROUND,AIR,WATER)
    Mass
    NewMass
} 

void App::SimulateCompression()
{
float Flow = 0;
float remainingmass = 0;

int ID = 0;

//Calculate and apply flow for each block
for (int X = 0; X < MAP_WIDTH; X++)
{
    for(int Y = 0; Y < MAP_HEIGHT; Y++)
    {
        //Skip inert ground blocks
        if(TileList[ID].TileProp == TILE_GROUND) continue;

        //Custom push-only flow
        Flow = 0;
        remainingmass = TileList[ID].Mass;
        if(remainingmass <= 0) continue;

        //The block below this one
        if(TileList[Rect2Lin(TILE_SIZE,X,(Y-1))].TileProp != TILE_GROUND)
        {
            Flow = GetStableWaterState(remainingmass + TileList[Rect2Lin(TILE_SIZE,X,(Y-1))].Mass /*mass[x][y-1]*/) - TileList[Rect2Lin(TILE_SIZE,X,(Y-1))].Mass;
            if(Flow > MinFlow){Flow *= 0.5; /*leads to smoother flow*/}
            int tempA = Min(MaxSpeed, remainingmass);
            if(Flow > tempA){Flow = tempA;}
            if(Flow < 0){Flow = 0;}

            TileList[ID].NewMass -= Flow;
            TileList[Rect2Lin(TILE_SIZE,X,(Y-1))].NewMass += Flow;
            remainingmass -= Flow;
        }

        if(remainingmass <= 0) continue;

        //Left
        if(TileList[Rect2Lin(TILE_SIZE,(X - 1),Y)].TileProp != TILE_GROUND)
        {
            //Equalize the amount of water in this block and it's neighbour
            Flow = (TileList[ID].Mass - TileList[Rect2Lin(TILE_SIZE,(X - 1),Y)].Mass)/4;
            if(Flow > MinFlow){Flow *= 0.5;}
            if(Flow > remainingmass){Flow = remainingmass;}
            if(Flow < 0){Flow = 0;}

            TileList[ID].NewMass -= Flow;
            TileList[Rect2Lin(TILE_SIZE,(X - 1),Y)].NewMass += Flow;
            remainingmass -= Flow;
        }

        if(remainingmass <= 0) continue;

        //Right
        if(TileList[Rect2Lin(TILE_SIZE,(X + 1),Y)].TileProp != TILE_GROUND)
        {
            //Equalize the amount of water in this block and it's neighbour
            Flow = (TileList[ID].Mass - TileList[Rect2Lin(TILE_SIZE,(X + 1),Y)].Mass)/4;
            if(Flow > MinFlow){Flow *= 0.5;}

            if(Flow > remainingmass){Flow = remainingmass;}
            if(Flow < 0){Flow = 0;}

            TileList[ID].NewMass -= Flow;
            TileList[Rect2Lin(TILE_SIZE,(X + 1),Y)].NewMass += Flow;
            remainingmass -= Flow;
        }

        if(remainingmass <= 0) continue;

        //Up. Only compressed water flows upwards
        if(TileList[Rect2Lin(TILE_SIZE,X,(Y + 1))].TileProp != TILE_GROUND)
        {
            Flow = remainingmass - GetStableWaterState(remainingmass + TileList[Rect2Lin(TILE_SIZE,X,(Y + 1))].Mass);
            if (Flow > MinFlow){Flow *= 0.5;}

            int tempB = Min(MaxSpeed, remainingmass);
            if(Flow > tempB){Flow = tempB;}
            if(Flow < 0){Flow = 0;}

            TileList[ID].NewMass -= Flow;
            TileList[Rect2Lin(TILE_SIZE,X,(Y + 1))].NewMass += Flow;
            remainingmass -= Flow;
        }
        ID++;
    }
}

ID = 0;
//Copy the new mass values 
for (int X = 0; X < MAP_WIDTH; X++)
{
    for (int Y = 0; Y < MAP_HEIGHT; Y++)
    {
        TileList[ID].Mass = TileList[ID].NewMass;
        ID++;
    }
}

ID = 0;
for(int X = 0; X < MAP_WIDTH; X++)
{
    for(int Y = 0; Y < MAP_HEIGHT; Y++)
    {
        //Skip ground blocks
        if(TileList[ID].TileProp == TILE_GROUND) continue;

        //Flag/unflag water blocks
        if(TileList[ID].Mass > MinMass)
        {
            TileList[ID].TileProp = TILE_WATER;
        }else
        {
            TileList[ID].TileProp = TILE_AIR;
        }
        ID++;
    }
}

//Remove any water that has left the map
for(int X = 0; X < MAP_WIDTH; X++)
{
    TileList[X].Mass = 0;
    TileList[Rect2Lin(TILE_SIZE,X,MAP_HEIGHT - 1)].Mass = 0;

}

for(int Y = 0; Y < MAP_HEIGHT; Y++)
{
    TileList[Rect2Lin(TILE_SIZE,0,Y)].Mass = 0;
    TileList[Rect2Lin(TILE_SIZE,(MAP_WIDTH - 1),Y)].Mass = 0;
}

}
genpfault
  • 51,148
  • 11
  • 85
  • 139
Yasin Khalil
  • 59
  • 1
  • 7
  • Can you be a bit more specific about what problem you're having (i.e. Tiles::Mass isn't changing for any tiles, some tiles are changing but others aren't, Tiles::NewMass isn't changing, etc.)? Have you attached a debugger or put in debugging statements to see what's actually running? – Nate Kohl Nov 02 '12 at 16:08
  • Yeah, I checked. The masses aren't transferring. – Yasin Khalil Nov 02 '12 at 16:13
  • Nothing happens. The Water blocks stay in their start up position. They do not move or transfer their mass to other tiles to simulate water movement. Nothing happens. I dont know what's going on. – Yasin Khalil Nov 02 '12 at 16:16
  • I recommend replacing some of the unwieldy expressions like `TileList[Rect2Lin(TILE_SIZE,X,(Y-1))]` with references: `Tile& below = TileList[Rect2Lin(TILE_SIZE,X,(Y-1))]` – genpfault Nov 02 '12 at 20:35
  • 1
    Is this a 2D implementation of [Cellular Automata for Physical Modelling](http://home.comcast.net/~tom_forsyth/papers/cellular_automata_for_physical_modelling.html)? – genpfault Nov 02 '12 at 20:46
  • 1
    It's based on this tutorial written in java. http://w-shadow.com/blog/2009/09/01/simple-fluid-simulation/ However, Iam using a vector of tiles instead of an array for tiles, mass, and new mass. – Yasin Khalil Nov 02 '12 at 21:03
  • This is the function im using to convert the X and Y components so that I can get the correct Element of the vector that corresponds to the components. int Rect2Lin(int w, int x, int y){ return y*w+x;} – Yasin Khalil Nov 02 '12 at 21:35
  • Have you stepped through it in debugger? – hyde Nov 02 '12 at 22:10
  • You might consider a [`std::valarray` wrapper](http://stackoverflow.com/a/2188001/44729). – genpfault Nov 02 '12 at 22:55
  • Yes, I have stepped through it. Not really sure what's going on. This is really frustrating > – Yasin Khalil Nov 03 '12 at 00:23
  • Ok, I have been doing tests all day! Something odd is happening. It only goes through the if statements inside the double nested for loop twice. Meaning ID = 0 and ID = 1. After that it completely skips the rest! What the heck is going on? That's 766 other elements inside the vector that its skipping!! !!Why? – Yasin Khalil Nov 03 '12 at 02:08
  • AHhhh I FIGURED IT OUT!! !!! THIS FREAKING LINE IS MAKING IT SKIP THE INCREMENT OF THE ID!! !!!!! "if(remainingmass <= 0) continue;" So it stays on 2 and doesnt go further down the vector. so all tiles after 1 have no mass!!! !! ! ! – Yasin Khalil Nov 03 '12 at 02:11
  • OK, now ID wont increment past 34!!! It resets to 0 along with the 2 for loops??? I'm soo confused! I changed all of the checks to this if(remainingmass <= 0){ID++;continue;} – Yasin Khalil Nov 03 '12 at 02:18
  • Ok, so ID is going to 0 because after ID hits 34 It just leaves the two nested for loops... Why would it do that? – Yasin Khalil Nov 03 '12 at 02:43

1 Answers1

2

Ok, so ID is going to 0 because after ID hits 34 It just leaves the two nested for loops... Why would it do that?

//Calculate and apply flow for each block
for (int X = 0; X < MAP_WIDTH; X++)
{
    for(int Y = 0; Y < MAP_HEIGHT; Y++)
    {
        //Skip inert ground blocks
        if(TileList[ID].TileProp == TILE_GROUND) continue;

        ...

        ID++;
    }
}

TileList[34] is probably a ground tile. At which point you hit that first if over and over (since you never get to the ID++ at the very end of the loop) until you have exhausted the for-loops.

Try this:

//Calculate and apply flow for each block
for (int X = 0; X < MAP_WIDTH; X++)
{
    for(int Y = 0; Y < MAP_HEIGHT; Y++)
    {
        int ID = Rect2Lin(TILE_SIZE,X,Y));

        //Skip inert ground blocks
        if(TileList[ID].TileProp == TILE_GROUND) continue;

        ...
    }
}

EDIT:

Ok, this works on my system:

#include <GL/glut.h>
#include <vector>

using namespace std;

// simple Eigen::Matrix work-alike
template< typename T >
class Matrix
{
public:
    Matrix( const size_t rows, const size_t cols ) 
        : mStride( cols )
        , mHeight( rows )
        , mStorage( rows * cols ) 
    {}

    T& operator()( const size_t row, const size_t col )
    {
        return mStorage[ row * mStride + col ];
    }

    const T& operator()( const size_t row, const size_t col ) const 
    {
        return mStorage[ row * mStride + col ];
    }

    size_t rows() const { return mHeight; }
    size_t cols() const { return mStride; }

private:
    vector< T > mStorage;
    size_t mStride;
    size_t mHeight;
};

struct Cell
{
    enum Type{ AIR, GROUND, WATER };

    Cell() 
        : mType( AIR )
        , mMass( 0 )
        , mNewMass( 0 )
    {}

    Type mType;
    float mMass;
    float mNewMass;
};

const float MaxMass = 1.0f; 
const float MinMass = 0.0001f; 
const float MaxCompress = 0.02f; 
const float MaxSpeed = 1.0f;
const float MinFlow = 0.01f;

//Take an amount of water and calculate how it should be split among two
//vertically adjacent cells. Returns the amount of water that should be in 
//the bottom cell. 
float get_stable_state_b( float total_mass )
{
    if ( total_mass <= 1 )
    {
        return 1;
    } 
    else if ( total_mass < 2*MaxMass + MaxCompress )
    {
        return (MaxMass*MaxMass + total_mass*MaxCompress)/(MaxMass + MaxCompress);
    } 
    else
    {
        return (total_mass + MaxCompress)/2;
    }
}

template< typename T >
T constrain( const T& val, const T& minVal, const T& maxVal )
{
    return max( minVal, min( val, maxVal ) );
}

typedef Matrix< Cell > State;
void stepState( State& cur )
{
    for( size_t y = 1; y < cur.rows()-1; ++y )
    {
        for( size_t x = 1; x < cur.cols()-1; ++x )
        {
            Cell& center = cur( y, x );

            // Skip inert ground blocks
            if( center.mType == Cell::GROUND )
                continue;

            // Custom push-only flow
            float Flow = 0;
            float remaining_mass = center.mMass;
            if( remaining_mass <= 0 )
                continue;

            // The block below this one
            Cell& below = cur( y-1, x );
            if( below.mType != Cell::GROUND )
            {
                Flow = get_stable_state_b( remaining_mass + below.mMass ) - below.mMass;
                if( Flow > MinFlow )
                {
                    //leads to smoother flow
                    Flow *= 0.5;
                }
                Flow = constrain( Flow, 0.0f, min(MaxSpeed, remaining_mass) );

                center.mNewMass -= Flow;
                below.mNewMass += Flow;
                remaining_mass -= Flow;
            }

            if ( remaining_mass <= 0 ) 
                continue;

            // Left
            Cell& left = cur( y, x-1 );
            if ( left.mType != Cell::GROUND )
            {
                // Equalize the amount of water in this block and it's neighbour
                Flow = ( center.mMass - left.mMass ) / 4;
                if ( Flow > MinFlow )
                {
                    Flow *= 0.5;
                }
                Flow = constrain(Flow, 0.0f, remaining_mass);
                center.mNewMass -= Flow;
                left.mNewMass += Flow;
                remaining_mass -= Flow;
            }

            if ( remaining_mass <= 0 ) 
                continue;

            // Right
            Cell& right = cur( y, x+1 );
            if ( right.mType != Cell::GROUND )
            {
                // Equalize the amount of water in this block and it's neighbour
                Flow = ( center.mMass - right.mMass ) / 4;
                if ( Flow > MinFlow )
                {
                    Flow *= 0.5;
                }
                Flow = constrain(Flow, 0.0f, remaining_mass);
                center.mNewMass -= Flow;
                right.mNewMass += Flow;
                remaining_mass -= Flow;
            }

            if ( remaining_mass <= 0 ) 
                continue;

            // The block above this one
            Cell& above = cur( y+1, x );
            if( above.mType != Cell::GROUND )
            {
                Flow = remaining_mass - get_stable_state_b( remaining_mass + above.mMass );
                if( Flow > MinFlow )
                {
                    //leads to smoother flow
                    Flow *= 0.5;
                }
                Flow = constrain( Flow, 0.0f, min(MaxSpeed, remaining_mass) );

                center.mNewMass -= Flow;
                above.mNewMass += Flow;
                remaining_mass -= Flow;
            }
        }
    }

    for( size_t y = 0; y < cur.rows(); ++y )
    {
        for( size_t x = 0; x < cur.cols(); ++x )
        {
            cur( y, x ).mMass = cur( y, x ).mNewMass;
        }
    }

    for( size_t y = 0; y < cur.rows(); ++y )
    {
        for( size_t x = 0; x < cur.cols(); ++x )
        {
            Cell& center = cur( y, x );
            if( center.mType == Cell::GROUND ) 
            {
                center.mMass = center.mNewMass = 0.0f;
                continue;
            }
            if( center.mMass > MinMass )
            {
                center.mType = Cell::WATER;
            }
            else
            {
                center.mType = Cell::AIR;
                center.mMass = 0.0f;
            }
        }
    }

    // Remove any water that has left the map
    for( size_t x = 0; x < cur.cols(); ++x )
    {
        cur( 0, x ).mMass = 0;
        cur( cur.rows()-1, x ).mMass = 0;
    }
    for( size_t y = 0; y < cur.rows(); ++y )
    {
        cur( y, 0 ).mMass = 0;
        cur( y, cur.cols()-1 ).mMass = 0;
    }
}

void showState( const State& state )
{
    glPolygonMode( GL_FRONT, GL_LINE );
    glBegin( GL_QUADS );
    glColor3ub( 0, 0, 0 );
    for( size_t y = 0; y < state.rows(); ++y )
    {
        for( size_t x = 0; x < state.cols(); ++x )
        {
            glVertex2f( x+0, y+0 );
            glVertex2f( x+1, y+0 );
            glVertex2f( x+1, y+1 );
            glVertex2f( x+0, y+1 );
        }
    }
    glEnd();

    glPolygonMode( GL_FRONT, GL_FILL );
    glBegin( GL_QUADS );
    for( size_t y = 0; y < state.rows(); ++y )
    {
        for( size_t x = 0; x < state.cols(); ++x )
        {
            if( state( y, x ).mType == Cell::AIR )
                continue;

            float height = 1.0f;
            if( state( y, x ).mType == Cell::GROUND )
            {
                glColor3ub( 152, 118, 84 );
            }
            else
            {
                glColor3ub( 0, 135, 189 );
                height = min( 1.0f, state( y, x ).mMass );
            }

            glVertex2f( x+0, y );
            glVertex2f( x+1, y );
            glVertex2f( x+1, y + height );
            glVertex2f( x+0, y + height );
        }
    }
    glEnd();
}

State state( 20, 20 );
void mouse( int button, int button_state, int x, int y )
{
    float pctX = (float)x / glutGet( GLUT_WINDOW_WIDTH );
    float pctY = 1.0f - ( (float)y / glutGet( GLUT_WINDOW_HEIGHT ) );
    size_t cellX = pctX * state.cols();
    size_t cellY = pctY * state.rows();
    Cell& cur = state( cellY, cellX );

    if( button_state == GLUT_UP )
        return;

    if( button == GLUT_LEFT_BUTTON )
    {
        cur.mType = ( cur.mType == Cell::GROUND ? Cell::AIR : Cell::GROUND );
        cur.mMass = cur.mNewMass = 0.0f;
    }

    if( button == GLUT_RIGHT_BUTTON )
    {
        cur.mType = Cell::WATER;
        cur.mMass = cur.mNewMass = 1.0f;
    }
}


void display()
{
    static bool firstTime = true;
    if( firstTime )
    {
        firstTime = false;
        for( size_t y = 0; y < state.rows(); ++y )
        {
            for( size_t x = 0; x < state.cols(); ++x )
            {
                state( y, x ).mType = (Cell::Type)( rand() % 3 );
                state( y, x ).mMass = 1.0f;
                state( y, x ).mNewMass = 1.0f;
            }
        }  
    }

    glClearColor( 1, 1, 1, 1 );
    glClear( GL_COLOR_BUFFER_BIT );

    glMatrixMode( GL_PROJECTION );
    glLoadIdentity();
    glOrtho( 0, state.cols(), 0, state.rows(), -1, 1);

    glMatrixMode( GL_MODELVIEW );
    glLoadIdentity();

    stepState( state );
    showState( state );

    glutSwapBuffers();
}

void timer(int extra)
{
    glutPostRedisplay();
    glutTimerFunc(16, timer, 0);
}

int main( int argc, char **argv )
{
    glutInit( &argc, argv );
    glutInitDisplayMode( GLUT_RGBA | GLUT_DOUBLE );
    glutInitWindowSize( 640, 480 );
    glutCreateWindow( "Cells" );
    glutDisplayFunc( display );
    glutMouseFunc( mouse );
    glutTimerFunc(0, timer, 0);
    glutMainLoop();
    return 0;
}
genpfault
  • 51,148
  • 11
  • 85
  • 139
  • Ok so I worked through your suggestion. It goes through the loops correctly and increments the current ID of the vector. So I was able to get further and found that the masses are never going below 1. SO this is why there is no visible simulation. So now, I just have to look through and figure out why the masses will not go below 1 when checking left, right, up, and down. – Yasin Khalil Nov 03 '12 at 23:51
  • Do you have any suggestions looking at the algorithm for "flow"? – Yasin Khalil Nov 03 '12 at 23:52
  • Wow, you really out did yourself. This is great. I'm going to spend time trying to understand your code and compare it to mine. You really out did yourself here. I honestly wasn't expecting a response. You not only gave me a response, you gave me an answer! Thanks A LOT! – Yasin Khalil Nov 06 '12 at 04:35
  • Again thanks, I looked through your simulation code and its exactly the same with your own method of a cell matrix. I'm going to try and implement your method. I still don't understand why my method of a vector of type class tile wont work! – Yasin Khalil Nov 06 '12 at 04:58
  • 1
    Yeah, sorry for the re-write. I figured a "clean room" transliteration of the Java/Processing code would be the quickest way to get something that worked correctly. Your direct array method *should* work, there's probably just a missing a minus sign or something somewhere :( – genpfault Nov 06 '12 at 05:36
  • So I tried to integrate your code into mine. Basically I made the matrix class and did everything the way you did it except the only difference is how I'm drawing it(which is with SDL) and Still the same problem as in that video i posted a few comments back. I can't believe it. I'm using SDL because my other app I have been developing for several months is in SDL. I dont see how SDL could be causing this issue! – Yasin Khalil Nov 07 '12 at 04:40
  • 1
    Can you post your complete program on pastebin or something? Perhaps something other than the simulation is somehow interfering. – genpfault Nov 07 '12 at 05:28
  • Ok here is the link to my project. I included a folder with all of the SDL stuff. You just have to change the directories in the project to that new folder i made. http://dpstudiosla.com/Water%20Simulation%20-%20for%20Genpfault.zip – Yasin Khalil Nov 08 '12 at 06:42
  • 1
    Well, for starters you have `Tile::Mass` and `Tile::NewMass` declared as `int`, not `float`. Looks like you're using `int` instead of `float` in some other places too, like `App::DrawWaterRectangle()`. – genpfault Nov 09 '12 at 02:38
  • 1
    Yeah that fixed it. I can't believe this entire problem was caused due to the fact that I had mass as an int. Do you know how terrible I feel atm lol? Seriously WTF. I think i'm going to take my screen and throw it out the window. Thanks, and I'm sorry for wasting your time. I wish there was a way I can commend you for your efforts. – Yasin Khalil Nov 09 '12 at 03:01
  • Heh, no worries. In the future when you ask questions try to post a [SSCCE](http://meta.stackexchange.com/questions/22754/sscce-how-to-provide-examples-for-programming-questions) so people can see *exactly* what you're seeing. – genpfault Nov 09 '12 at 03:47