v0.6.3: rewrote shadowcasting logic, 3-10x more efficient

This commit is contained in:
Evan Debenham 2017-12-17 21:35:34 -05:00
parent 2009095a66
commit 409af754ec
2 changed files with 84 additions and 92 deletions
core/src/main/java/com/shatteredpixel/shatteredpixeldungeon
actors/hero
mechanics

View File

@ -473,13 +473,17 @@ public class Hero extends Char {
//calls to dungeon.observe will also update hero's local FOV. //calls to dungeon.observe will also update hero's local FOV.
fieldOfView = Dungeon.level.heroFOV; fieldOfView = Dungeon.level.heroFOV;
//do a full observe (including fog update) if not resting.
if (!resting || buff(MindVision.class) != null || buff(Awareness.class) != null) { if (!ready) {
Dungeon.observe(); //do a full observe (including fog update) if not resting.
} else { if (!resting || buff(MindVision.class) != null || buff(Awareness.class) != null) {
//otherwise just directly re-calculate FOV Dungeon.observe();
Dungeon.level.updateFieldOfView( this, fieldOfView ); } else {
//otherwise just directly re-calculate FOV
Dungeon.level.updateFieldOfView(this, fieldOfView);
}
} }
checkVisibleMobs(); checkVisibleMobs();
if (buff(FlavourBuff.class) != null) { if (buff(FlavourBuff.class) != null) {
BuffIndicator.refreshHero(); BuffIndicator.refreshHero();

View File

@ -24,12 +24,13 @@ package com.shatteredpixel.shatteredpixeldungeon.mechanics;
import com.shatteredpixel.shatteredpixeldungeon.Dungeon; import com.shatteredpixel.shatteredpixeldungeon.Dungeon;
import com.shatteredpixel.shatteredpixeldungeon.utils.BArray; import com.shatteredpixel.shatteredpixeldungeon.utils.BArray;
//based on: http://www.roguebasin.com/index.php?title=FOV_using_recursive_shadowcasting
public final class ShadowCaster { public final class ShadowCaster {
private static final int MAX_DISTANCE = 8; private static final int MAX_DISTANCE = 8;
private static boolean[] falseArray;
//max length of rows as FOV moves out, for each FOV distance
//This is used to make the overall FOV circular, instead of square
private static int[][] rounding; private static int[][] rounding;
static { static {
rounding = new int[MAX_DISTANCE+1][]; rounding = new int[MAX_DISTANCE+1][];
@ -45,104 +46,91 @@ public final class ShadowCaster {
BArray.setFalse(fieldOfView); BArray.setFalse(fieldOfView);
//set source cell to true
fieldOfView[y * Dungeon.level.width() + x] = true; fieldOfView[y * Dungeon.level.width() + x] = true;
boolean[] losBlocking = Dungeon.level.losBlocking; boolean[] losBlocking = Dungeon.level.losBlocking;
Obstacles obs = new Obstacles();
//scans octants, clockwise
scanSector( distance, fieldOfView, losBlocking, obs, x, y, +1, +1, 0, 0 ); scanOctant(distance, fieldOfView, losBlocking, 1, x, y, 0.0, 1.0, +1, -1, false);
scanSector( distance, fieldOfView, losBlocking, obs, x, y, -1, +1, 0, 0 ); scanOctant(distance, fieldOfView, losBlocking, 1, x, y, 0.0, 1.0, -1, +1, true);
scanSector( distance, fieldOfView, losBlocking, obs, x, y, +1, -1, 0, 0 ); scanOctant(distance, fieldOfView, losBlocking, 1, x, y, 0.0, 1.0, +1, +1, true);
scanSector( distance, fieldOfView, losBlocking, obs, x, y, -1, -1, 0, 0 ); scanOctant(distance, fieldOfView, losBlocking, 1, x, y, 0.0, 1.0, +1, +1, false);
scanSector( distance, fieldOfView, losBlocking, obs, x, y, 0, 0, +1, +1 ); scanOctant(distance, fieldOfView, losBlocking, 1, x, y, 0.0, 1.0, -1, +1, false);
scanSector( distance, fieldOfView, losBlocking, obs, x, y, 0, 0, -1, +1 ); scanOctant(distance, fieldOfView, losBlocking, 1, x, y, 0.0, 1.0, +1, -1, true);
scanSector( distance, fieldOfView, losBlocking, obs, x, y, 0, 0, +1, -1 ); scanOctant(distance, fieldOfView, losBlocking, 1, x, y, 0.0, 1.0, -1, -1, true);
scanSector( distance, fieldOfView, losBlocking, obs, x, y, 0, 0, -1, -1 ); scanOctant(distance, fieldOfView, losBlocking, 1, x, y, 0.0, 1.0, -1, -1, false);
} }
//FIXME This is is the primary performance bottleneck for game logic, need to optimize or rewrite
private static void scanSector( int distance, boolean[] fieldOfView, boolean[] losBlocking, Obstacles obs, int cx, int cy, int m1, int m2, int m3, int m4 ) {
obs.reset();
for (int p=1; p <= distance; p++) {
float dq2 = 0.5f / p;
int pp = rounding[distance][p];
for (int q=0; q <= pp; q++) {
int x = cx + q * m1 + p * m3;
int y = cy + p * m2 + q * m4;
if (y >= 0 && y < Dungeon.level.height() && x >= 0 && x < Dungeon.level.width()) {
float a0 = (float)q / p;
float a1 = a0 - dq2;
float a2 = a0 + dq2;
int pos = y * Dungeon.level.width() + x;
if (obs.isBlocked( a0 ) && obs.isBlocked( a1 ) && obs.isBlocked( a2 )) { //TODO this is slightly less permissive that the previous algorithm, decide if that's okay
// Do nothing
} else {
fieldOfView[pos] = true;
}
if (losBlocking[pos]) {
obs.add( a1, a2 );
}
}
}
obs.nextRow();
}
}
private static final class Obstacles { //scans a single 45 degree octant of the FOV.
//This can add up to a whole FOV by mirroring in X(mX), Y(mY), and X=Y(mXY)
private static void scanOctant(int distance, boolean[] fov, boolean[] blocking, int row,
int x, int y, double lSlope, double rSlope,
int mX, int mY, boolean mXY){
private static int SIZE = (MAX_DISTANCE+1) * (MAX_DISTANCE+1) / 2; boolean inBlocking = false;
private float[] a1 = new float[SIZE]; int start, end;
private float[] a2 = new float[SIZE]; int col;
private int length; //calculations are offset by 0.5 because FOV is coming from the center of the source cell
private int limit;
public void reset() { //for each row, starting with the current one
length = 0; for (; row <= distance; row++){
limit = 0;
}
public void add( float o1, float o2 ) {
if (length > limit && o1 <= a2[length-1]) { //we offset by slightly less than 0.5 to account for slopes just touching a cell
if (lSlope == 0) start = 0;
// Merging several blocking cells else start = (int)Math.floor((row - 0.5) * lSlope + 0.499);
a2[length-1] = o2;
} else {
a1[length] = o1;
a2[length++] = o2;
}
} if (rSlope == 1) end = rounding[distance][row];
else end = Math.min( rounding[distance][row],
public boolean isBlocked( float a ) { (int)Math.ceil((row + 0.5) * rSlope - 0.499));
for (int i=0; i < limit; i++) {
if (a >= a1[i] && a <= a2[i]) { //coordinates of source
return true; int cell = x + y*Dungeon.level.width();
//plus coordinates of current cell (including mirroring in x, y, and x=y)
if (mXY) cell += mX*start*Dungeon.level.width() + mY*row;
else cell += mX*start + mY*row*Dungeon.level.width();
//for each column in this row, which
for (col = start; col <= end; col++){
fov[cell] = true;
if (blocking[cell]){
if (!inBlocking){
inBlocking = true;
//start a new scan, 1 row deeper, ending at the left side of current cell
if (col != start){
scanOctant(distance, fov, blocking, row+1, x, y, lSlope,
//change in x over change in y
(col - 0.5) / (row + 0.5),
mX, mY, mXY);
}
}
} else {
if (inBlocking){
inBlocking = false;
//restrict current scan to the left side of current cell for future rows
//change in x over change in y
lSlope = (col - 0.5) / (row - 0.5);
}
} }
if (!mXY) cell += mX;
else cell += mX*Dungeon.level.width();
} }
return false;
} //if the row ends in a blocking cell, this scan is finished.
if (inBlocking) return;
public void nextRow() {
limit = length;
} }
} }
} }