From c290e5fe47101657b2d95c147e96684b8577fe2f Mon Sep 17 00:00:00 2001 From: Evan Debenham Date: Tue, 26 Dec 2017 16:46:44 -0500 Subject: [PATCH] v0.6.3: significant refactoring and performance improvements to fog of war --- .../src/main/java/com/watabou/utils/Rect.java | 9 + core/src/main/assets/wall_blocking.png | Bin 152 -> 162 bytes .../shatteredpixeldungeon/Dungeon.java | 63 +++-- .../scenes/GameScene.java | 22 +- .../shatteredpixeldungeon/tiles/FogOfWar.java | 262 +++++++++++------- .../tiles/WallBlockingTilemap.java | 172 ++++++++---- 6 files changed, 324 insertions(+), 204 deletions(-) diff --git a/SPD-classes/src/main/java/com/watabou/utils/Rect.java b/SPD-classes/src/main/java/com/watabou/utils/Rect.java index 808d7631b..7a6b9aace 100644 --- a/SPD-classes/src/main/java/com/watabou/utils/Rect.java +++ b/SPD-classes/src/main/java/com/watabou/utils/Rect.java @@ -99,6 +99,15 @@ public class Rect { return result; } + public Rect union( Rect other ){ + Rect result = new Rect(); + result.left = Math.min( left, other.left ); + result.right = Math.max( right, other.right ); + result.top = Math.min( top, other.top ); + result.bottom = Math.max( bottom, other.bottom ); + return result; + } + public Rect union( int x, int y ) { if (isEmpty()) { return set( x, y, x + 1, y + 1 ); diff --git a/core/src/main/assets/wall_blocking.png b/core/src/main/assets/wall_blocking.png index 3f92535837ede4e03936b9d333d2d2c2a627e7ed..be1a4f012b2ec7dd028baad4fb5fedbaef86cd5c 100644 GIT binary patch delta 100 zcmbQixQKCr7AwQ5US*?+`tl(>QlgA(jJ#(jF)%O~d%8G=cpOjubH0H~;v(w>MrEn_ zAzw2ZIujcXu3&bPHF(7!<`#APL%j(Xh|}Qsj)6h=DvQz)F_|Esb_P#ZKbLh*2~7Yv CX&_es delta 90 zcmV-g0Hyz;0hj@hDh2 toUpdate; + private volatile ArrayList updating; + //should be divisible by 2 private static final int PIX_PER_TILE = 2; + /* + TODO currently the center of each fox pixel is aligned with the inside of a cell + might be possible to create a better fog effect by aligning them with edges of a cell, + similar to the existing fog effect in vanilla (although probably with more precision) + the advantage here is that it may be possible to totally eliminate the tile blocking map + */ + public FogOfWar( int mapWidth, int mapHeight ) { super(); @@ -113,26 +122,47 @@ public class FogOfWar extends Image { DungeonTilemap.SIZE / PIX_PER_TILE, DungeonTilemap.SIZE / PIX_PER_TILE); - updated = new Rect(0, 0, mapWidth, mapHeight); + toUpdate = new ArrayList<>(); + toUpdate.add(new Rect(0, 0, mapWidth, mapHeight)); } public synchronized void updateFog(){ - updated.set( 0, 0, mapWidth, mapHeight ); + toUpdate.clear(); + toUpdate.add(new Rect(0, 0, mapWidth, mapHeight)); + } + + public synchronized void updateFog(Rect update){ + for (Rect r : toUpdate.toArray(new Rect[0])){ + if (!r.intersect(update).isEmpty()){ + toUpdate.remove(r); + toUpdate.add(r.union(update)); + return; + } + } + toUpdate.add(update); } - public synchronized void updateFogCell( int cell ){ - updateFogArea( cell % mapWidth , cell / mapWidth, 1, 1 ); + public synchronized void updateFog( int cell, int radius ){ + Rect update = new Rect( + (cell % mapWidth) - radius, + (cell / mapWidth) - radius, + (cell % mapWidth) - radius + 1 + 2*radius, + (cell / mapWidth) - radius + 1 + 2*radius); + update.left = Math.max(0, update.left); + update.top = Math.max(0, update.top); + update.right = Math.min(mapWidth, update.right); + update.bottom = Math.min(mapHeight, update.bottom); + if (update.isEmpty()) return; + updateFog( update ); } public synchronized void updateFogArea(int x, int y, int w, int h){ - updated.union(x, y); - updated.union(x + w, y + h); - updated = updated.intersect( new Rect(0, 0, mapWidth, mapHeight) ); + updateFog(new Rect(x, y, x + w, y + h)); } - public synchronized void moveToUpdating(){ - updating = new Rect(updated); - updated.setEmpty(); + private synchronized void moveToUpdating(){ + updating = toUpdate; + toUpdate = new ArrayList<>(); } private boolean[] visible; @@ -147,101 +177,116 @@ public class FogOfWar extends Image { this.brightness = ShatteredPixelDungeon.brightness() + 2; moveToUpdating(); - - boolean fullUpdate = updating.height() == mapHeight && updating.width() == mapWidth; + + boolean fullUpdate = false; + if (updating.size() == 1){ + Rect update = updating.get(0); + if (update.height() == mapHeight && update.width() == mapWidth){ + fullUpdate = true; + } + } FogTexture fog = (FogTexture)texture; int cell; - int[] colorArray = new int[PIX_PER_TILE*PIX_PER_TILE]; - for (int i=updating.top; i < updating.bottom; i++) { - cell = mapWidth * i + updating.left; - for (int j=updating.left; j < updating.right; j++) { - - if (cell >= Dungeon.level.length()) continue; //do nothing - - if (!Dungeon.level.discoverable[cell] - || (!visible[cell] && !visited[cell] && !mapped[cell])){ - //we skip filling cells here if it isn't a full update - // because they must already be dark - if (fullUpdate) - fillCell(j, i, FOG_COLORS[INVISIBLE][brightness]); - cell++; - continue; - } - - //wall tiles - if (DungeonTileSheet.wallStitcheable(Dungeon.level.map[cell])){ - - //internal wall tiles - if (cell + mapWidth >= mapLength){ - fillCell(j, i, FOG_COLORS[INVISIBLE][brightness]); - - } else if (DungeonTileSheet.wallStitcheable(Dungeon.level.map[cell + mapWidth])){ - - //these tiles need to check both the left and right side, to account for only one half of them being seen - if (cell % mapWidth != 0){ - - //picks the darkest fog between current tile, left, and below-left(if left is a wall). - if (cell + mapWidth < mapLength && DungeonTileSheet.wallStitcheable(Dungeon.level.map[cell - 1])){ - - //if below-left is also a wall, then we should be dark no matter what. - if (DungeonTileSheet.wallStitcheable(Dungeon.level.map[cell + mapWidth - 1])){ - colorArray[0] = colorArray[2] = FOG_COLORS[INVISIBLE][brightness]; - } else { - colorArray[0] = colorArray[2] = FOG_COLORS[Math.max(getCellFog(cell), Math.max(getCellFog(cell + mapWidth - 1), getCellFog(cell - 1)))][brightness]; - } - - } else { - colorArray[0] = colorArray[2] = FOG_COLORS[Math.max(getCellFog(cell), getCellFog(cell - 1))][brightness]; - } - - } else { - colorArray[0] = colorArray [2] = FOG_COLORS[INVISIBLE][brightness]; - } - - if ((cell+1) % mapWidth != 0){ - - //picks the darkest fog between current tile, right, and below-right(if right is a wall). - if (cell + mapWidth < mapLength && DungeonTileSheet.wallStitcheable(Dungeon.level.map[cell + 1])){ - - //if below-right is also a wall, then we should be dark no matter what. - if (DungeonTileSheet.wallStitcheable(Dungeon.level.map[cell + mapWidth + 1])){ - colorArray[1] = colorArray[3] = FOG_COLORS[INVISIBLE][brightness]; - } else { - colorArray[1] = colorArray[3] = FOG_COLORS[Math.max(getCellFog(cell), Math.max(getCellFog(cell + mapWidth + 1), getCellFog(cell + 1)))][brightness]; - } - - } else { - colorArray[1] = colorArray[3] = - FOG_COLORS[Math.max(getCellFog(cell), getCellFog(cell + 1))][brightness]; - } - - } else { - colorArray[1] = colorArray [3] = FOG_COLORS[INVISIBLE][brightness]; - } - - fillCell(j, i, colorArray); - - //camera-facing wall tiles - } else { - fillCell(j, i, FOG_COLORS[Math.max(getCellFog(cell), getCellFog(cell + mapWidth))][brightness]); + + for (Rect update : updating) { + for (int i = update.top; i <= update.bottom; i++) { + cell = mapWidth * i + update.left; + for (int j = update.left; j <= update.right; j++) { + + if (cell >= Dungeon.level.length()) continue; //do nothing + + if (!Dungeon.level.discoverable[cell] + || (!visible[cell] && !visited[cell] && !mapped[cell])) { + //we skip filling cells here if it isn't a full update + // because they must already be dark + if (fullUpdate) + fillCell(j, i, FOG_COLORS[INVISIBLE][brightness]); + cell++; + continue; } - - //other tiles - } else { - fillCell(j, i, FOG_COLORS[getCellFog(cell)][brightness]); + + //wall tiles + if (wall(cell)) { + + //always dark if nothing is beneath them + if (cell + mapWidth >= mapLength) { + fillCell(j, i, FOG_COLORS[INVISIBLE][brightness]); + + //internal wall tiles, need to check both the left and right side, + // to account for only one half of them being seen + } else if (wall(cell + mapWidth)) { + + //left side + if (cell % mapWidth != 0) { + + //picks the darkest fog between current tile, left, and below-left(if left is a wall). + if (wall(cell - 1)) { + + //if below-left is also a wall, then we should be dark no matter what. + if (wall(cell + mapWidth - 1)) { + fillLeft(j, i, FOG_COLORS[INVISIBLE][brightness]); + } else { + fillLeft(j, i, FOG_COLORS[Math.max(getCellFog(cell), Math.max(getCellFog(cell + mapWidth - 1), getCellFog(cell - 1)))][brightness]); + } + + } else { + fillLeft(j, i, FOG_COLORS[Math.max(getCellFog(cell), getCellFog(cell - 1))][brightness]); + } + + } else { + fillLeft(j, i, FOG_COLORS[INVISIBLE][brightness]); + } + + //right side + if ((cell + 1) % mapWidth != 0) { + + //picks the darkest fog between current tile, right, and below-right(if right is a wall). + if (wall(cell + 1)) { + + //if below-right is also a wall, then we should be dark no matter what. + if (wall(cell + mapWidth + 1)) { + fillRight(j, i, FOG_COLORS[INVISIBLE][brightness]); + } else { + fillRight(j, i, FOG_COLORS[Math.max(getCellFog(cell), Math.max(getCellFog(cell + mapWidth + 1), getCellFog(cell + 1)))][brightness]); + } + + } else { + fillRight(j, i, FOG_COLORS[Math.max(getCellFog(cell), getCellFog(cell + 1))][brightness]); + } + + } else { + fillRight(j, i, FOG_COLORS[INVISIBLE][brightness]); + } + + //camera-facing wall tiles + //darkest between themselves and the tile below them + } else { + fillCell(j, i, FOG_COLORS[Math.max(getCellFog(cell), getCellFog(cell + mapWidth))][brightness]); + } + + //other tiles, just their direct value + } else { + fillCell(j, i, FOG_COLORS[getCellFog(cell)][brightness]); + } + + cell++; } - - cell++; } + + } + + if (updating.size() == 1 && !fullUpdate){ + fog.update(updating.get(0).top * PIX_PER_TILE, updating.get(0).bottom * PIX_PER_TILE); + } else { + fog.update(); } - if (updating.width() == mapWidth && updating.height() == mapHeight) - fog.update(); - else - fog.update(updating.top * PIX_PER_TILE, updating.bottom * PIX_PER_TILE); - + } + + private boolean wall(int cell) { + return DungeonTileSheet.wallStitcheable(Dungeon.level.map[cell]); } private int getCellFog( int cell ){ @@ -256,13 +301,23 @@ public class FogOfWar extends Image { return INVISIBLE; } } - - private void fillCell( int x, int y, int[] colors){ + + private void fillLeft( int x, int y, int color){ FogTexture fog = (FogTexture)texture; for (int i = 0; i < PIX_PER_TILE; i++){ fog.pixels.position(((y * PIX_PER_TILE)+i)*width2 + x * PIX_PER_TILE); - for (int j = 0; j < PIX_PER_TILE; j++) { - fog.pixels.put(colors[i*PIX_PER_TILE + j]); + for (int j = 0; j < PIX_PER_TILE/2; j++) { + fog.pixels.put(color); + } + } + } + + private void fillRight( int x, int y, int color){ + FogTexture fog = (FogTexture)texture; + for (int i = 0; i < PIX_PER_TILE; i++){ + fog.pixels.position(((y * PIX_PER_TILE)+i)*width2 + x * PIX_PER_TILE + PIX_PER_TILE/2); + for (int j = PIX_PER_TILE/2; j < PIX_PER_TILE; j++) { + fog.pixels.put(color); } } } @@ -356,9 +411,8 @@ public class FogOfWar extends Image { @Override public void draw() { - if (!updated.isEmpty()){ + if (!toUpdate.isEmpty()){ updateTexture(Dungeon.level.heroFOV, Dungeon.level.visited, Dungeon.level.mapped); - updating.setEmpty(); } super.draw(); diff --git a/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/tiles/WallBlockingTilemap.java b/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/tiles/WallBlockingTilemap.java index ac2e0749d..bf27cef03 100644 --- a/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/tiles/WallBlockingTilemap.java +++ b/core/src/main/java/com/shatteredpixel/shatteredpixeldungeon/tiles/WallBlockingTilemap.java @@ -30,11 +30,12 @@ public class WallBlockingTilemap extends Tilemap { public static final int SIZE = 16; - private static final int CLEARED = -1; - private static final int BLOCK_NONE = 0; - private static final int BLOCK_RIGHT = 1; - private static final int BLOCK_LEFT = 2; - private static final int BLOCK_ALL = 3; + private static final int CLEARED = -2; + private static final int BLOCK_NONE = -1; + private static final int BLOCK_RIGHT = 0; + private static final int BLOCK_LEFT = 1; + private static final int BLOCK_ALL = 2; + private static final int BLOCK_BELOW = 3; public WallBlockingTilemap() { super("wall_blocking.png", new TextureFilm( "wall_blocking.png", SIZE, SIZE ) ); @@ -45,109 +46,158 @@ public class WallBlockingTilemap extends Tilemap { public synchronized void updateMap() { super.updateMap(); data = new int[size]; //clears all values, including cleared tiles - for (int i = 0; i < data.length; i++) - updateMapCell(i); + + for (int cell = 0; cell < data.length; cell++) { + //force all none-discoverable and border cells to cleared + if (!Dungeon.level.discoverable[cell] || + !Dungeon.level.insideMap(cell)){ + data[cell] = CLEARED; + } else { + updateMapCell(cell); + } + } } + private int curr; + @Override public synchronized void updateMapCell(int cell) { - int prev = data[cell]; - int curr; + + //TODO should doors be considered? currently the blocking is a bit permissive around doors - if (prev == CLEARED){ - return; + //non-wall tiles + if (!wall(cell)) { - } else if (!Dungeon.level.discoverable[cell]) { - curr = CLEARED; - - //handles blocking wall overhang (which is technically on a none wall tile) - } else if (!wall(cell)) { - - if (!fogHidden(cell)) { + //clear empty floor tiles and cells which are visible + if (!fogHidden(cell) || !wall(cell + mapWidth)) { curr = CLEARED; - } else if ( wall(cell + mapWidth) && !fogHidden(cell + mapWidth) - && fogHidden(cell - 1) && fogHidden(cell + 1)) { - curr = BLOCK_ALL; + //block wall overhang if: + //- The cell below is a wall and visible + //- All of left, below-left, right, below-right is either a wall or hidden + } else if ( !fogHidden(cell + mapWidth) + && (fogHidden(cell - 1) || wall(cell - 1)) + && (fogHidden(cell + 1) || wall(cell + 1)) + && (fogHidden(cell - 1 + mapWidth) || wall(cell - 1 + mapWidth)) + && (fogHidden(cell + 1 + mapWidth) || wall(cell + 1 + mapWidth))) { + curr = BLOCK_BELOW; } else { curr = BLOCK_NONE; } + //wall tiles } else { - if (fogHidden(cell - mapWidth) && fogHidden(cell) && fogHidden(cell + mapWidth)) { - curr = BLOCK_NONE; + //camera-facing wall tiles + if (!wall(cell + mapWidth)) { - //camera-facing wall tiles - } else if (!wall(cell + mapWidth)) { - - if (!fogHidden(cell + mapWidth)){ + //Block a camera-facing wall if: + //- the cell above, above-left, or above-right is not a wall, visible, and has a wall below + //- none of the remaining 5 neighbour cells are both not a wall and visible + + //if all 3 above are wall we can shortcut and just clear the cell + if (wall(cell - 1 - mapWidth) && wall(cell - mapWidth) && wall(cell + 1 - mapWidth)){ curr = CLEARED; - - } else if ((cell + 1) % mapWidth != 0 && !fogHidden(cell + 1) - && !door(cell + 1) && !(wall(cell + 1) && wall(cell + 1 + mapWidth))){ - curr = CLEARED; - - } else if (cell % mapWidth != 0 && !fogHidden(cell - 1) - && !door(cell - 1) && !(wall(cell - 1) && wall(cell - 1 + mapWidth))){ - curr = CLEARED; - + + } else if ((!wall(cell - 1 - mapWidth) && !fogHidden(cell - 1 - mapWidth) && wall(cell - 1)) || + (!wall(cell - mapWidth) && !fogHidden(cell - mapWidth)) || + (!wall(cell + 1 - mapWidth) && !fogHidden(cell + 1 - mapWidth) && wall(cell+1))){ + + if ( !fogHidden( cell + mapWidth) || + (!wall(cell - 1) && !fogHidden(cell - 1)) || + (!wall(cell - 1 + mapWidth) && !fogHidden(cell - 1 + mapWidth)) || + (!wall(cell + 1) && !fogHidden(cell + 1)) || + (!wall(cell + 1 + mapWidth) && !fogHidden(cell + 1 + mapWidth))){ + curr = CLEARED; + } else { + curr = BLOCK_ALL; + } + } else { - curr = BLOCK_ALL; + curr = BLOCK_NONE; } - //internal wall tiles + //internal wall tiles } else { + + //Block the side of an internal wall if: + //- the cell above, below, or the cell itself is visible + //and all of the following are NOT true: + //- the top-side neighbour is visible and the side neighbour isn't a wall. + //- the side neighbour is both not a wall and visible + //- the bottom-side neighbour is both not a wall and visible curr = BLOCK_NONE; - - if ((cell + 1) % mapWidth != 0) { - if ((wall(cell + 1) || fogHidden(cell + 1 - mapWidth)) - && fogHidden(cell + 1) - && (wall(cell + 1 + mapWidth) || fogHidden(cell + 1 + mapWidth))){ + + if (!fogHidden(cell - mapWidth) + || !fogHidden(cell) + || !fogHidden(cell + mapWidth)) { + + //right side + if ((!wall(cell + 1) && !fogHidden(cell + 1 - mapWidth)) || + (!wall(cell + 1) && !fogHidden(cell + 1)) || + (!wall(cell + 1 + mapWidth) && !fogHidden(cell + 1 + mapWidth)) + ){ + //do nothing + } else { curr += 1; } - } - - if (cell % mapWidth != 0) { - if ((wall(cell - 1) || fogHidden(cell - 1 - mapWidth)) - && fogHidden(cell - 1) - && (wall(cell - 1 + mapWidth) || fogHidden(cell - 1 + mapWidth))){ + + //left side + if ((!wall(cell - 1) && !fogHidden(cell - 1 - mapWidth)) || + (!wall(cell - 1) && !fogHidden(cell - 1)) || + (!wall(cell - 1 + mapWidth) && !fogHidden(cell - 1 + mapWidth)) + ){ + //do nothing + } else { curr += 2; } - } - - if (curr == BLOCK_NONE) { - curr = CLEARED; + + if (curr == BLOCK_NONE) { + curr = CLEARED; + } } } } - if (prev != curr){ + if (data[cell] != curr){ data[cell] = curr; super.updateMapCell(cell); } } private boolean fogHidden(int cell){ - if (cell < 0 || cell >= Dungeon.level.length()) return false; - if (!Dungeon.level.visited[cell] && !Dungeon.level.mapped[cell]) return true; - if (wall(cell) && cell + mapWidth < Dungeon.level.length() && !wall(cell + mapWidth) && - !Dungeon.level.visited[cell + mapWidth] && !Dungeon.level.mapped[cell + mapWidth]) + if (!Dungeon.level.visited[cell] && !Dungeon.level.mapped[cell]) { return true; + } else if (wall(cell) && !wall(cell + mapWidth) && + !Dungeon.level.visited[cell + mapWidth] && !Dungeon.level.mapped[cell + mapWidth]) { + return true; + } return false; } - //for the purposes of wall stitching, tiles below the map count as walls private boolean wall(int cell) { - return cell >= 0 && (cell >= size || DungeonTileSheet.wallStitcheable(Dungeon.level.map[cell])); + return DungeonTileSheet.wallStitcheable(Dungeon.level.map[cell]); } private boolean door(int cell) { - return cell >= 0 && cell < size && DungeonTileSheet.doorTile(Dungeon.level.map[cell]); + return DungeonTileSheet.doorTile(Dungeon.level.map[cell]); + } + + public synchronized void updateArea(int cell, int radius){ + int l = cell%mapWidth - radius; + int t = cell/mapWidth - radius; + int r = cell%mapWidth + radius; + int b = cell/mapWidth + radius; + updateArea( + Math.max(0, l), + Math.max(0, t), + Math.min(mapWidth-1, r - l), + Math.min(mapHeight-1, b - t) + ); } public synchronized void updateArea(int x, int y, int w, int h) {