class SimulationWorld { public byte[][] tiles = null; //the map array public int world_width = 0; public int world_height = 0; public long simulationFrameCount = 0; public long activeTiles = 0; public byte update_mask; public SimulationWorld(){ //empty... } public SimulationWorld( int the_width, int the_height ){ //Initialize the map this.initMap( the_width, the_height ); } //Advance the simulation by one step public void stepSimulation(){ simulationFrameCount++; activeTiles = 0; //Figure out the "update" bit-flag value for this frame update_mask = byte ( ( simulationFrameCount & (long)1 ) << 7 ); byte updated_water = byte(water | update_mask); byte unupdated_water = byte( updated_water ^ update_mask ); byte updated_stillwater = byte(water | update_mask | staticTile); //When water moves diagonally, it should pick the direction randomly. //However, calculating a random number for every single water tile is too slow, //so instead we just switch between prefering left and right diagonals on each timestep int horizontal_dir = (update_mask != 0) ? 1 : -1; byte thisTile; //Loop over the entire map, bottom-up for ( int y = 1; y <= world_height; y++ ){ int y_minus_1 = y - 1; //'tis minor optimization for ( int x = 1; x <= world_width; x++ ){ //Skip static blocks if ( isStatic(x, y) ) continue; //Skip water tiles that have already been udpated if ( tiles[x][y] == updated_water ) continue; activeTiles++; //useful for evaluating performance thisTile = byte(tiles[x][y] & clearUpdateMask); switch ( thisTile ){ case water: //Water always flows down, if it can. Otherwise, it may flow diagonally down if //the direct neighbour isn't water (workaround; the down-flowing water should get //priority, so we can't let other tiles flow in under it) and the diagonal neighbour //is empty. if ( tiles[x][y_minus_1] == air ){ //Down tiles[x][y_minus_1] = updated_water; tiles[x][y] = air; this.activateNeighbours( x, y ); } else if ( (tiles[x - horizontal_dir][y_minus_1] == air) && ( (tiles[x - horizontal_dir][y] & clearAllMask) != water) ){ //One diagonal tiles[x - horizontal_dir][y_minus_1] = updated_water; tiles[x][y] = air; this.activateNeighbours( x, y ); } else if ( (tiles[x + horizontal_dir][y_minus_1] == air) && ( (tiles[x + horizontal_dir][y] & clearAllMask) != water) ){ //The other diagonal tiles[x + horizontal_dir][y_minus_1] = updated_water; tiles[x][y] = air; this.activateNeighbours( x, y ); } else { //Can't move anywhere, so deactivate this water tile tiles[x][y] = updated_stillwater; } break; case faucet: //Faucets generate water below them if ( tiles[x][y_minus_1] == air ) { tiles[x][y_minus_1] = updated_water; } break; default: break; } } } //Remove anything that might have left the world clearBorders(); } protected void initMap( int the_width, int the_height ){ //Initialize the map world_width = the_width; world_height = the_height; tiles = new byte[world_width + 2][world_height + 2]; clear(); } //Reset the world map by filling it with air (empty squares) public void clear(){ for ( int x = 0; x < this.world_width + 2; x++ ){ for ( int y = 0; y < this.world_height + 2; y++ ){ tiles[x][y] = air; } } } //Fill the map with random squares public void randomize(){ byte[] choices = new byte[3]; choices[0] = air; choices[1] = water; choices[2] = ground; for ( int x = 1; x <= this.world_width; x++ ){ for ( int y = 1; y <= this.world_height; y++ ){ tiles[x][y] = choices[ int(random(0, 3)) ]; } } } //Simulate the worst case scenario - fill the entire map with active tiles that must be //updated every timestep. public void worstCaseWorld(){ //Fill the top row with faucets for ( int x = 1; x <= world_width; x++ ){ tiles[x][world_height] = faucet; } //Fill the rest of the map with falling water for ( int x = 1; x <= world_width; x++ ){ for ( int y = 1; y < world_height; y++ ){ tiles[x][y] = water; } } } //Remove any tiles that might have left the world protected void clearBorders(){ for ( int x = 0; x < world_width + 2; x++ ){ tiles[x][0] = air; tiles[x][world_height + 1] = air; } for ( int y = 1; y < world_height + 1; y++ ){ tiles[0][y] = air; tiles[world_width + 1][y] = air; } } //Returns true if the tile at [x, y] is static public final boolean isStatic( int x, int y ){ return isStatic( tiles[x][y] ); } //Returns true if tileType represents a static tile public final boolean isStatic( byte tileType ){ return (tileType & staticTile ) != 0; } //Activate the neighbouring water tiles of the specified tile. //Basically, when you put a hole (air tile) in the map, we want the //water tiles above that hole to get activated and flow down into the //hole. protected void activateNeighbours( int x, int y ){ //The tile above this one if ( ( tiles[x][y + 1] & clearUpdateMask ) == stillwater ){ tiles[x][y + 1] = byte( tiles[x][y + 1] & (~staticTile) ); } //Above-left if ( ( tiles[x - 1][y + 1] & clearUpdateMask ) == stillwater ){ tiles[x - 1][y + 1] = byte( tiles[x - 1][y + 1] & (~staticTile) ); } //Above-right if ( ( tiles[x + 1][y + 1] & clearUpdateMask ) == stillwater ){ tiles[x + 1][y + 1] = byte( tiles[x + 1][y + 1] & (~staticTile) ); } } //Set a tile to a given value and update the activity grid public void setTile( int x, int y, byte newTile ){ byte oldTile = tiles[x][y]; tiles[x][y] = newTile; //If the new tile is air, activate the neighbouring water cells //so that one of them may flow into this tile when they're simulated if ( (newTile == air) && (oldTile != air) ){ activateNeighbours(x, y); } } public void display(){ display(1, 1, world_width, world_height); } //Draw the specified portion of the world public void display( int minx, int miny, int maxx, int maxy ){ minx = constrain(minx, 1, world_width); maxx = constrain(maxx, minx, world_width); miny = constrain(miny, 1, world_height); maxy = constrain(maxy, miny, world_height); drawAreaSize = (maxx - minx + 1)*(maxy - miny + 1); //stroke(50, 50, 50); //Uncomment to enable the grid //textAlign( CENTER, CENTER ); //Uncomment these two lines if you intend to //textFont(font, 15); //enable the "display tile ID" lines in drawTile. //Draw all visible tiles noStroke(); for( int x = minx; x <= maxx; x++ ){ for( int y = miny; y <= maxy; y++ ){ drawTile( x, y, tiles[x][y] ); } } //Draw the map border stroke( 0, 0, 0 ); noFill(); rect(0, 0, world_width * TILE_SIZE, world_height * TILE_SIZE ); } //Draw a single tile void drawTile( int worldX, int worldY, byte TileType ){ TileType = byte(TileType & clearUpdateMask); fill( tileColors[TileType] ); rect( (worldX-1)*TILE_SIZE, (world_height - worldY)*TILE_SIZE, TILE_SIZE, TILE_SIZE ); //Uncomment this to display the tile type ID on each tile //fill(0, 0, 0); //text( int( TileType & clearAllMask ), (worldX-1)*TILE_SIZE + TILE_SIZE/2, (world_height - worldY + 0.5)*TILE_SIZE ); } }