squidpony.squidgrid.FOVCache Maven / Gradle / Ivy
Show all versions of squidlib-util Show documentation
package squidpony.squidgrid;
import squidpony.annotation.GwtIncompatible;
import squidpony.squidgrid.mapping.DungeonUtility;
import squidpony.squidmath.Coord;
import squidpony.squidmath.NumberTools;
import squidpony.squidmath.OrderedMap;
import squidpony.squidmath.ShortVLA;
import java.util.*;
import java.util.concurrent.*;
import static squidpony.squidmath.CoordPacker.*;
/**
* A combined FOV calculator, partial LOS calculator, FOV/LOS compressor, and tool to store/query/extract compressed
* FOV/LOS data; it is less generally-useful than it sounds, and using {@link FOV} is usually easier and faster.
* This class operates on one level map at a time and stores FOV maps for all cells in a memory-efficient way,
* though it is likely to take too long to process large maps to be useful on those unless run before the player gets
* to that map. (Large here means more than 10,000 total cells, or 100 width * 100 height, but this rough upper bound is
* based on the capability of the machine running the calculations, and should be expected to be much lower on, for
* instance, older dual-core phones than newer quad-core desktops). There are a few ways to ensure FOVCache is done
* processing a map by the time a player gets to that map; the recommended approach for games with clearly defined,
* separate levels is to generate the first level as part of game startup, run cacheAll() immediately afterward (this
* will start calculation on a second thread), and when the map needs to be displayed (because gameplay has started and
* any character creation steps are done), to call the caching method's counterpart, awaitCache(), which will cause
* gameplay to be essentially frozen until the cache completes (you could display a loading message before calling it).
* The next part is more interesting; you should generate the second level immediately after the awaiting method
* finishes, before the player has approached the second level, and create another FOVCache object using the second
* level as its map, then call cacheAll() on the second level's FOVCache. This will calculate the cache, as before, on
* another thread, and you should call the appropriate awaiting method when the player is entering the second level
* (descending or ascending a staircase, for example). This time, as long as the player spent more than a few seconds on
* the first level for most maps, the cache should be pre-calculated and awaiting should take no time. When the game is
* closed or the player quits, you should call destroy() on this FOVCache to avoid threads possibly lingering after the
* game should have ended.
*
* The FOV calculation this class performs includes a post-processing stage that guarantees symmetry for both LOS and
* FOV. This works by checking every cell that is within the maximum radius for each non-wall cell, and if any cell A
* can see cell B, but cell B can not yet see A, then B's cached FOV map will be altered so it can see A. The other
* post-processing step provides distant lighting; if lights have been passed to the constructor as a Map of Coord keys
* to Integer values, then the Coords in that Map that are walkable squares will emit light up to a radius equal to
* their value in that Map. Cells with distant lighting will always be in FOV if they could be seen at up to radius
* equal to maxLOSRadius, which defaults to 62 and is calculated for every FOVCache as an LOS cache. Calculating distant
* lighting adds a somewhat substantial amount of time to each caching attempt, estimated at tripling the amount of time
* used in cases where there are very many lights in a large dungeon (in a 100x100 dungeon, with one light per 5x5 area
* when the center of that area is walkable, for example), but the lighting is expected to be much less of a performance
* hindrance on smaller maps (80x40, 60x60, anything smaller, etc.) or when there are simply less lights to process
* (because distant lighting is meant to go beyond nearby cells, it needs to run through essentially all lights for every
* cell it processes, and even though adding the lit area to FOV is very efficient and does not require recalculating
* FOV, having lots of lights means lots of work per cell).
*
* This class extends FOV and can be used as a replacement for FOV in some cases. Generally, FOVCache provides methods
* that allow faster manipulation and checks of certain values (such as a simple case of whether a cell can be seen from
* another cell at a given FOV radius), but will fall back to Shadowcasting FOV (without using the cache) if any calls
* to FOV methods are made that have not had their needed information cached. Uncached calls to FOV will not have some
* of the niceties FOVCache can provide, like distant lights. If different light levels are needed (which Shadowcasting
* does not provide), you can call Graded variants on the FOV methods, which will fall back to a Ripple FOV instead and
* will have values between 0.0 and 1.0 instead of only those two extremes.
*
* Conservation of memory usage is a primary concern for this class; storing a full 2D array for every cell on a map
* that is even moderately large uses outrageous amounts of RAM, and attempting that naive approach on a 256x256 map
* would use more than 4 GB of RAM for purely the data from storing bytes or booleans, not including the JVM's overhead
* of between 12 and 19 bytes for every array. Using smaller maps helps both this class and any other approach (less
* cells to store FOV for), and at least for FOVCache, using smaller maxRadius values can reduce memory usage as well.
* For a normal 100x100 map, storing one byte[][] for every cell, and storing a 2D array of those (which has a minimal
* effect on memory consumption vs. a 1D array in this case), the resulting byte[][][][] will use 112,161,616 bytes of
* RAM, approximately 110 MB; this would still need an additional step of processing when used to limit a requested
* FOV map stored in it to the appropriate vision range. To contrast, the current version of FOVCache on the same size
* of map, caching 12 separate FOV radii, uses approximately 6.2 MB. Tests run on ten 100x100 dungeons ran the gamut
* between 6,049,760 and 6,404,336 bytes (the layout of the dungeon doesn't affect memory usage of the naive case, but
* it does have a small effect on the compressed version). To actually use the compressed maps does take an additional
* processing step, but careful benchmarking indicates running FOV for a roughly 12 radius (Radius.SQUARE kind) area
* takes twice as long as simply extracting a cached FOV map, and the advantage for the cache is greater for larger FOV
* radii (but the cache also uses slightly more memory). This compares against the fastest FOV type, Shadowcasting, but
* to get distance information from an FOV you need to use either the customized FOV algorithm in this class (Slope
* Shadowcasting), or to use the Ripple FOV type. Ripple does respect translucent objects, which neither shadowcasting
* nor this class' slope shadowcasting does, but getting 16 FOV levels from the cache for every walkable cell on a
* 100x100 dungeon map takes approximately 19 ms while running Ripple FOV for the same set of cells takes over 700 ms.
* Benchmarks are conducted using JMH, a tool developed by the OpenJDK team, in a Maven module called
* squidlib-performance that is not distributed with SquidLib but is available if you download the SquidLib source code.
* However, because the benchmarks don't take the initial calculations into account, they can be somewhat misleading;
* there's also overhead and complexity involved with the background threads this uses. Newer benchmarks that test some
* more recent additions to FOV, like {@link FOV#reuseFOV(double[][], double[][], int, int, double)}, show those methods
* as faster than retrieving a compressed FOV map from FOVCache, largely because they can avoid creating garbage.
*
* @see squidpony.squidmath.CoordPacker has various utilities for operating on compressed data of this kind.
* Created by Tommy Ettinger on 10/7/2015.
* @author Tommy Ettinger
*/
@GwtIncompatible
public class FOVCache extends FOV {
protected int maxRadius, maxLOSRadius;
protected int width;
protected int height;
protected int mapLimit;
protected int limit;
protected double[][] resMap;
protected Radius radiusKind;
protected short[][][] cache;
protected short[][][] tmpCache;
protected short[][] losCache;
protected boolean complete, qualityComplete, refreshComplete;
protected FOV fov, gradedFOV;
protected short[][] ALL_WALLS;
protected short[] wallMap;
protected double[][] atan2Cache, directionAngles;
protected short[][] distanceCache;
protected Coord[][] waves;
protected final int NUM_THREADS;
private ExecutorService executor;
protected double fovPermissiveness;
protected OrderedMap lights;
protected Coord[] lightSources;
protected int[] lightBrightnesses;
private double[][] levels;
protected double decay;
private Thread performanceThread = null, qualityThread = null;
private static final double HALF_PI = Math.PI * 0.5, QUARTER_PI = Math.PI * 0.25125,
SLIVER_PI = Math.PI * 0.05, PI2 = Math.PI * 2;
/**
* Create an FOVCache for a given map (as a char[][]), caching all FOV radii from 0 up to and including maxRadius,
* using the given Radius enum to determine FOV shape. Upon calling cacheAllPerformance() or cacheAll(),
* (collectively, the caching methods), the object this creates will run a medium-quality, fairly permissive FOV
* calculation for every cell on the map using 8 threads, and if cacheAll() was called, will then ensure
* symmetry (if cell A can see cell B, then it will make cell B able to see cell A even if it couldn't in an earlier
* step). At the same time as the first caching method call, this will calculate Line of Sight at maxLOSRadius (here
* it is given a default of 62) for all cells. Walls will always have no cells in their FOV or LOS.
* @param map a char[][] as returned by SquidLib's map generators
* @param maxRadius the longest radius that will possibly need to be cached for FOV; LOS is separate
* @param radiusKind a Radius enum that determines the shape of each FOV area
*/
public FOVCache(char[][] map, int maxRadius, Radius radiusKind)
{
if(map == null || map.length == 0)
throw new UnsupportedOperationException("The map used by FOVCache must not be null or empty");
init();
NUM_THREADS = 8;
executor = Executors.newFixedThreadPool(NUM_THREADS);
width = map.length;
height = map[0].length;
if(width > 256 || height > 256)
throw new UnsupportedOperationException("Map size is too large to efficiently cache, aborting");
mapLimit = width * height;
if(maxRadius <= 0 || maxRadius >= 63)
throw new UnsupportedOperationException("FOV radius is incorrect. Must be 0 < maxRadius < 63");
fov = new FOV(FOV.SHADOW);
gradedFOV = new FOV(RIPPLE);
resMap = DungeonUtility.generateResistances(map);
this.maxRadius = Math.max(1, maxRadius);
this.maxLOSRadius = 62;
decay = 1.0 / maxRadius;
this.radiusKind = radiusKind;
fovPermissiveness = 0.9;
lights = new OrderedMap<>();
cache = new short[mapLimit][][];
tmpCache = new short[mapLimit][][];
losCache = new short[mapLimit][];
ALL_WALLS = new short[maxRadius + 1][];
for (int i = 0; i < maxRadius + 1; i++) {
ALL_WALLS[i] = ALL_WALL;
}
limit = 0x10000;
if(height <= 128) {
limit >>= 1;
if (width <= 128) {
limit >>= 1;
if (width <= 64) {
limit >>= 1;
if (height <= 64) {
limit >>= 1;
if (height <= 32) {
limit >>= 1;
if (width <= 32) {
limit >>= 1;
}
}
}
}
}
}
preloadMeasurements();
complete = false;
}
/**
* Create an FOVCache for a given map (as a char[][]), caching all FOV radii from 0 up to and including maxRadius,
* using the given Radius enum to determine FOV shape. Upon calling cacheAllPerformance() or cacheAll(),
* (collectively, the caching methods), the object this creates will run a medium-quality, fairly permissive FOV
* calculation for every cell on the map using a number of threads equal to threadCount, and if cacheAll()
* was called, will then ensure symmetry (if cell A can see cell B, then it will make cell B able to see cell A even
* if it couldn't in an earlier step). At the same time as the first caching method call, this will calculate Line
* of Sight at maximum range (given by maxLOSRadius) for all cells. Walls will always have no cells in their FOV or
* in their LOS.
* @param map a char[][] as returned by SquidLib's map generators
* @param maxRadius the longest radius that will possibly need to be cached for FOV; LOS is separate
* @param maxLOSRadius the longest radius that will possibly need to be cached for LOS, must be less than 63
* @param radiusKind a Radius enum that determines the shape of each FOV area
* @param threadCount how many threads to use during the full-map calculations
*/
public FOVCache(char[][] map, int maxRadius, int maxLOSRadius, Radius radiusKind, int threadCount)
{
if(map == null || map.length == 0)
throw new UnsupportedOperationException("The map used by FOVCache must not be null or empty");
init();
NUM_THREADS = threadCount;
executor = Executors.newFixedThreadPool(NUM_THREADS);
width = map.length;
height = map[0].length;
if(width > 256 || height > 256)
throw new UnsupportedOperationException("Map size is too large to efficiently cache, aborting");
mapLimit = width * height;
if(maxRadius <= 0 || maxRadius >= 63)
throw new UnsupportedOperationException("FOV radius is incorrect. Must be 0 < maxRadius < 63");
if(maxLOSRadius <= 0 || maxLOSRadius >= 63)
throw new UnsupportedOperationException("LOS radius is incorrect. Must be 0 < maxLOSRadius < 63");
fov = new FOV(FOV.SHADOW);
gradedFOV = new FOV(RIPPLE);
resMap = DungeonUtility.generateResistances(map);
this.maxRadius = Math.max(1, maxRadius);
this.maxLOSRadius = Math.max(1, maxLOSRadius);
decay = 1.0 / maxRadius;
this.radiusKind = radiusKind;
fovPermissiveness = 0.9;
lights = new OrderedMap<>();
cache = new short[mapLimit][][];
tmpCache = new short[mapLimit][][];
losCache = new short[mapLimit][];
ALL_WALLS = new short[maxRadius][];
for (int i = 0; i < maxRadius; i++) {
ALL_WALLS[i] = ALL_WALL;
}
limit = 0x10000;
if(height <= 128) {
limit >>= 1;
if (width <= 128) {
limit >>= 1;
if (width <= 64) {
limit >>= 1;
if (height <= 64) {
limit >>= 1;
if (height <= 32) {
limit >>= 1;
if (width <= 32) {
limit >>= 1;
}
}
}
}
}
}
preloadMeasurements();
complete = false;
}
/**
* Create an FOVCache for a given map (as a char[][]), caching all FOV radii from 0 up to and including maxRadius,
* using the given Radius enum to determine FOV shape. Upon calling cacheAllPerformance() or cacheAll(),
* (collectively, the caching methods), the object this creates will run a medium-quality, fairly permissive FOV
* calculation for every cell on the map using a number of threads equal to threadCount, and if cacheAll()
* was called, will then ensure symmetry (if cell A can see cell B, then it will make cell B able to see cell A even
* if it couldn't in an earlier step). At the same time as the first caching method call, this will calculate Line
* of Sight at maximum range (given by maxLOSRadius) for all cells. Walls will always have no cells in their FOV or
* in their LOS. This constructor also allows you to initialize light sources in the level using the lights
* parameter; any Coord keys should correspond to walkable cells (or they will be ignored), and the values will be
* the range those cells should light up..
* @param map a char[][] as returned by SquidLib's map generators
* @param maxRadius the longest radius that will possibly need to be cached for FOV; LOS is separate
* @param maxLOSRadius the longest radius that will possibly need to be cached for LOS, must be less than 63
* @param radiusKind a Radius enum that determines the shape of each FOV area
* @param threadCount how many threads to use during the full-map calculations
* @param lights a Map of Coords (which should be walkable) to the radii they should light (not for moving lights)
*/
public FOVCache(char[][] map, int maxRadius, int maxLOSRadius, Radius radiusKind, int threadCount, Map lights)
{
if(map == null || map.length == 0)
throw new UnsupportedOperationException("The map used by FOVCache must not be null or empty");
init();
NUM_THREADS = threadCount;
executor = Executors.newFixedThreadPool(NUM_THREADS);
width = map.length;
height = map[0].length;
if(width > 256 || height > 256)
throw new UnsupportedOperationException("Map size is too large to efficiently cache, aborting");
mapLimit = width * height;
if(maxRadius <= 0 || maxRadius >= 63)
throw new UnsupportedOperationException("FOV radius is incorrect. Must be 0 < maxRadius < 63");
if(maxLOSRadius <= 0 || maxLOSRadius >= 63)
throw new UnsupportedOperationException("LOS radius is incorrect. Must be 0 < maxLOSRadius < 63");
fov = new FOV(FOV.SHADOW);
gradedFOV = new FOV(RIPPLE);
resMap = DungeonUtility.generateResistances(map);
this.maxRadius = Math.max(1, maxRadius);
this.maxLOSRadius = Math.max(1, maxLOSRadius);
decay = 1.0 / maxRadius;
this.radiusKind = radiusKind;
fovPermissiveness = 0.9;
this.lights = new OrderedMap<>(lights);
cache = new short[mapLimit][][];
tmpCache = new short[mapLimit][][];
losCache = new short[mapLimit][];
ALL_WALLS = new short[maxRadius][];
for (int i = 0; i < maxRadius; i++) {
ALL_WALLS[i] = ALL_WALL;
}
limit = 0x10000;
if(height <= 128) {
limit >>= 1;
if (width <= 128) {
limit >>= 1;
if (width <= 64) {
limit >>= 1;
if (height <= 64) {
limit >>= 1;
if (height <= 32) {
limit >>= 1;
if (width <= 32) {
limit >>= 1;
}
}
}
}
}
}
preloadMeasurements();
complete = false;
}
private void preloadLights()
{
Iterator it = lights.keySet().iterator();
Coord pos;
while (it.hasNext())
{
pos = it.next();
if(resMap[pos.x][pos.y] >= 1.0)
it.remove();
}
lightSources = lights.keySet().toArray(new Coord[lights.size()]);
lightBrightnesses = new int[lights.size()];
for (int i = 0; i < lightSources.length; i++) {
lightBrightnesses[i] = lights.get(lightSources[i]);
}
}
private void preloadMeasurements()
{
levels = new double[maxRadius + 1][maxRadius + 1];
//levels[maxRadius][maxRadius] = 1.0;
for (int i = 1; i <= maxRadius; i++) {
System.arraycopy(generateLightLevels(i), 0, levels[i], maxRadius - i + 1, i);
}
boolean[][] walls = new boolean[width][height];
for (int i = 0; i < width; i++) {
for (int j = 0; j < height; j++) {
walls[i][j] = resMap[i][j] >= 1.0;
}
}
wallMap = pack(walls);
preloadLights();
atan2Cache = new double[maxRadius * 2 + 1][maxRadius * 2 + 1];
distanceCache = new short[maxRadius * 2 + 1][maxRadius * 2 + 1];
waves = new Coord[maxRadius + 1][];
waves[0] = new Coord[]{Coord.get(maxRadius, maxRadius)};
ShortVLA[] positionsAtDistance = new ShortVLA[maxRadius + 1];
for (int i = 0; i < maxRadius + 1; i++) {
positionsAtDistance[i] = new ShortVLA(i * 8 + 1);
}
short tmp, inverse_tmp;
for (int i = 0; i <= maxRadius; i++) {
for (int j = 0; j <= maxRadius; j++) {
tmp = distance(i, j);
inverse_tmp = (short)(maxRadius + 1 - tmp / 2);
atan2Cache[maxRadius + i][maxRadius + j] = NumberTools.atan2(j, i);
if(atan2Cache[maxRadius + i][maxRadius + j] < 0)
atan2Cache[maxRadius + i][maxRadius + j] += PI2;
if(tmp > 0) {
atan2Cache[maxRadius - i][maxRadius + j] = NumberTools.atan2(j, -i);
if(atan2Cache[maxRadius - i][maxRadius + j] < 0)
atan2Cache[maxRadius - i][maxRadius + j] += PI2;
atan2Cache[maxRadius + i][maxRadius - j] = NumberTools.atan2(-j, i);
if(atan2Cache[maxRadius + i][maxRadius - j] < 0)
atan2Cache[maxRadius + i][maxRadius - j] += PI2;
atan2Cache[maxRadius - i][maxRadius - j] = NumberTools.atan2(-j, -i);
if(atan2Cache[maxRadius - i][maxRadius - j] < 0)
atan2Cache[maxRadius - i][maxRadius - j] += PI2;
}
if(tmp / 2 <= maxRadius && inverse_tmp > 0) {
distanceCache[maxRadius + i][maxRadius + j] = inverse_tmp;
if (tmp > 0) {
distanceCache[maxRadius - i][maxRadius + j] = inverse_tmp;
distanceCache[maxRadius + i][maxRadius - j] = inverse_tmp;
distanceCache[maxRadius - i][maxRadius - j] = inverse_tmp;
positionsAtDistance[tmp / 2].add(zEncode((short) (maxRadius + i), (short) (maxRadius + j)));
if (i > 0)
positionsAtDistance[tmp / 2].add(zEncode((short) (maxRadius - i), (short) (maxRadius + j)));
if (j > 0)
positionsAtDistance[tmp / 2].add(zEncode((short) (maxRadius + i), (short) (maxRadius - j)));
if(i > 0 && j > 0)
positionsAtDistance[tmp / 2].add(zEncode((short) (maxRadius - i), (short) (maxRadius - j)));
}else {
positionsAtDistance[0].add(zEncode((short) maxRadius, (short) maxRadius));
}
}
}
}
short[][] positionsZ = new short[maxRadius + 1][];
for (int i = 0; i <= maxRadius; i++) {
positionsZ[i] = positionsAtDistance[i].toArray();
waves[i] = new Coord[positionsZ[i].length];
for (int j = 0; j < waves[i].length; j++) {
waves[i][j] = zDecode(positionsZ[i][j]);
}
}
directionAngles = new double[3][3];
directionAngles[0][0] = NumberTools.atan2(1,1);
directionAngles[0][1] = NumberTools.atan2(0,1);
directionAngles[0][2] = NumberTools.atan2(-1,1) + PI2;
directionAngles[1][0] = NumberTools.atan2(1,0);
directionAngles[1][1] = 0;
directionAngles[1][2] = NumberTools.atan2(-1,0) + PI2;
directionAngles[2][0] = NumberTools.atan2(1,-1);
directionAngles[2][1] = NumberTools.atan2(0,-1);
directionAngles[2][2] = NumberTools.atan2(-1,-1) + PI2;
}
/**
* Packs FOV for a point as a center, and returns it to be stored.
* @param index an int that stores the x,y center of FOV as calculated by: x + y * width
* @return a multi-packed series of progressively wider FOV radii
*/
protected long storeCellFOV(int index) {
long startTime = System.currentTimeMillis();
cache[index] = calculatePackedSlopeShadowFOV(index % width, index / width);
//cache[index] = calculatePackedExternalFOV(index % width, index / width);
return System.currentTimeMillis() - startTime;
}
/**
* Packs FOV for a point as a center, and returns it to be stored.
* @param index an int that stores the x,y center of FOV as calculated by: x + y * width
* @return a multi-packed series of progressively wider FOV radii
*/
protected long storeCellLOS(int index) {
long startTime = System.currentTimeMillis();
losCache[index] = calculatePackedLOS(index % width, index / width);
return System.currentTimeMillis() - startTime;
}
/**
* Uses previously cached FOV and makes it symmetrical. Also handles distant lights
* @param index an int that stores the x,y center of FOV as calculated by: x + y * width
* @return a multi-packed series of progressively wider FOV radii
*/
protected long storeCellSymmetry(int index) {
long startTime = System.currentTimeMillis();
tmpCache[index] = improveQuality(index % width, index / width);
return System.currentTimeMillis() - startTime;
}
/**
* Packs FOV for the given viewer's X and Y as a center, and returns the packed data to be stored.
* @param viewerX an int less than 256 and less than width
* @param viewerY an int less than 256 and less than height
* @return a multi-packed series of progressively wider FOV radii
*/
@SuppressWarnings("unused")
private short[][] calculatePackedExternalFOV(int viewerX, int viewerY)
{
if(viewerX < 0 || viewerY < 0 || viewerX >= width || viewerY >= height)
return ALL_WALLS;
if(resMap[viewerX][viewerY] >= 1.0)
{
return ALL_WALLS;
}
long on = 0, current = 0;
ShortVLA[] packing = new ShortVLA[maxRadius];
int[] skip = new int[maxRadius];
short x, y;
short[][] packed = new short[maxRadius][];
double[][] fovMap;
for(int l = 0; l < maxRadius; l++) {
fovMap = fov.calculateFOV(resMap, viewerX, viewerY, l + 1, radiusKind);
packing[l] = new ShortVLA(64);
for (int i = 0, ml = 0; i < limit && ml < mapLimit; i++, skip[l]++) {
x = hilbertX[i];
y = hilbertY[i];
if (x >= width || y >= height) {
if ((on & (1L << l)) != 0L) {
on ^= 1L << l;
packing[l].add((short) skip[l]);
skip[l] = 0;
}
continue;
}
ml++;
// sets the bit at position l in current to 1 if the following is true, 0 if it is false:
// fovMap[x][y] > levels[l]
// looks more complicated than it is.
current ^= ((fovMap[x][y] > 0.0 ? -1 : 0) ^ current) & (1 << l);
if (((current >> l) & 1L) != ((on >> l) & 1L)) {
packing[l].add((short) skip[l]);
skip[l] = 0;
on = current;
// sets the bit at position l in on to the same as the bit at position l in current.
on ^= (-((current >> l) & 1L) ^ on) & (1L << l);
}
}
if (((on >> l) & 1L) == 1L)
packing[l].add((short) skip[l]);
if(packing[l].size == 0)
packed[l] = ALL_WALL;
else
packed[l] = packing[l].toArray();
}
return packed;
}
/**
* Packs FOV for the given viewer's X and Y as a center, and returns the packed data to be stored.
* @param viewerX an int less than 256 and less than width
* @param viewerY an int less than 256 and less than height
* @return a packed FOV map for radius equal to maxLOSRadius
*/
public short[] calculatePackedLOS(int viewerX, int viewerY)
{
if(viewerX < 0 || viewerY < 0 || viewerX >= width || viewerY >= height)
return ALL_WALL;
if(resMap[viewerX][viewerY] >= 1.0)
{
return ALL_WALL;
}
return pack(fov.calculateFOV(resMap, viewerX, viewerY, maxLOSRadius, radiusKind));
}
/**
* Packs FOV for the given viewer's X and Y as a center, and returns the packed data to be stored.
* @param viewerX an int less than 256 and less than width
* @param viewerY an int less than 256 and less than height
* @return a multi-packed series of progressively wider FOV radii
*/
public short[][] calculatePackedSlopeShadowFOV(int viewerX, int viewerY) {
if (viewerX < 0 || viewerY < 0 || viewerX >= width || viewerY >= height)
return ALL_WALLS;
if (resMap[viewerX][viewerY] >= 1.0) {
return ALL_WALLS;
}
return packMulti(slopeShadowFOV(viewerX, viewerY), maxRadius + 1);
}
/**
* Packs FOV for the given viewer's X and Y as a center, and returns the packed data to be stored.
* @param viewerX an int less than 256 and less than width
* @param viewerY an int less than 256 and less than height
* @return a multi-packed series of progressively wider FOV radii
*/
public short[][] calculatePackedWaveFOV(int viewerX, int viewerY) {
if (viewerX < 0 || viewerY < 0 || viewerX >= width || viewerY >= height)
return ALL_WALLS;
if (resMap[viewerX][viewerY] >= 1.0) {
return ALL_WALLS;
}
return packMulti(waveFOV(viewerX, viewerY), maxRadius + 1);
}
public short[][] getCacheEntry(int x, int y)
{
return cache[x + y * width];
}
public short[] getCacheEntry(int x, int y, int radius)
{
return cache[x + y * width][maxRadius - radius];
}
public short[] getLOSEntry(int x, int y)
{
return losCache[x + y * width];
}
public boolean queryCache(int visionRange, int viewerX, int viewerY, int targetX, int targetY)
{
return queryPacked(cache[viewerX + viewerY * width][maxRadius - visionRange], targetX, targetY);
}
public boolean isCellVisible(int visionRange, int viewerX, int viewerY, int targetX, int targetY)
{
return queryPacked(cache[viewerX + viewerY * width][maxRadius - visionRange], targetX, targetY) ||
queryPacked(cache[targetX + targetY * width][maxRadius - visionRange], viewerX, viewerY);
}
public boolean queryLOS(int viewerX, int viewerY, int targetX, int targetY)
{
return queryPacked(losCache[viewerX + viewerY * width], targetX, targetY);
}
public static long arrayMemoryUsage(int length, long bytesPerItem)
{
return (((bytesPerItem * length + 12 - 1) >> 3) + 1) << 3;
}
@SuppressWarnings("unused")
public static long arrayMemoryUsage2D(int xSize, int ySize, long bytesPerItem)
{
return arrayMemoryUsage(xSize, (((bytesPerItem * ySize + 12 - 1) >> 3) + 1) << 3);
}
public static int arrayMemoryUsageJagged(short[][] arr)
{
int ctr = 0;
for (int i = 0; i < arr.length; i++) {
ctr += arrayMemoryUsage(arr[i].length, 2);
}
return (((ctr + 12 - 1) >> 3) + 1) << 3;
}
public long approximateMemoryUsage()
{
long ctr = 0;
for (int i = 0; i < cache.length; i++) {
ctr += arrayMemoryUsageJagged(cache[i]);
}
ctr = (((ctr + 12L - 1L) >> 3) + 1L) << 3;
ctr += (((arrayMemoryUsageJagged(losCache) + 12L - 1L) >> 3) + 1L) << 3;
return ctr;
}
/*
//needs rewrite, must store the angle a ray traveled at to get around an obstacle, and propagate it to the end of
//the ray. It should check if the angle theta for a given point is too different from the angle in angleMap.
private byte[][] waveFOVWIP(int viewerX, int viewerY) {
byte[][] gradientMap = new byte[width][height];
double[][] angleMap = new double[2 * maxRadius + 1][2 * maxRadius + 1];
for (int i = 0; i < angleMap.length; i++) {
Arrays.fill(angleMap[i], -20);
}
gradientMap[viewerX][viewerY] = (byte)(2 * maxRadius);
Direction[] dirs = (radiusKind == Radius.DIAMOND || radiusKind == Radius.OCTAHEDRON)
? Direction.CARDINALS : Direction.OUTWARDS;
int cx, cy, ccwAdjX, ccwAdjY, cwAdjX, cwAdjY, ccwGridX, ccwGridY, cwGridX, cwGridY;
Coord pt;
double theta, angleCW, angleCCW;
byte dist;
boolean blockedCCW, blockedCW, isStraightCCW;
for(int w = 0; w < waves.length; w++)
{
for(int c = 0; c < waves[w].length; c++)
{
pt = waves[w][c];
cx = viewerX - maxRadius + pt.x;
cy = viewerY - maxRadius + pt.y;
if(cx < width && cx >= 0 && cy < height && cy >= 0)
{
theta = atan2Cache[pt.x][pt.y];
dist = (byte)(distanceCache[pt.x][pt.y ]);
if(w <= 0)
{
gradientMap[cx][cy] = dist;
}
else {
switch ((int) Math.floor(theta / QUARTER_PI)) {
//positive x, postive y (lower on screen), closer to x-axis
case 0:
cwAdjX = pt.x - 1;
cwAdjY = pt.y;
angleCW = directionAngles[0][1];
isStraightCCW = false;
ccwAdjX = pt.x - 1;
ccwAdjY = pt.y - 1;
angleCCW = directionAngles[0][0];
break;
//positive x, postive y (lower on screen), closer to y-axis
case 1:
cwAdjX = pt.x - 1;
cwAdjY = pt.y - 1;
angleCW = directionAngles[0][0];
ccwAdjX = pt.x;
ccwAdjY = pt.y - 1;
angleCCW = directionAngles[1][0];
isStraightCCW = true;
break;
//negative x, postive y (lower on screen), closer to y-axis
case 2:
cwAdjX = pt.x;
cwAdjY = pt.y - 1;
angleCW = directionAngles[1][0];
isStraightCCW = false;
ccwAdjX = pt.x + 1;
ccwAdjY = pt.y - 1;
angleCCW = directionAngles[2][0];
break;
//negative x, postive y (lower on screen), closer to x-axis
case 3:
cwAdjX = pt.x + 1;
cwAdjY = pt.y - 1;
angleCW = directionAngles[2][0];
ccwAdjX = pt.x + 1;
ccwAdjY = pt.y;
angleCCW = directionAngles[2][1];
isStraightCCW = true;
break;
//negative x, negative y (higher on screen), closer to x-axis
case 4:
cwAdjX = pt.x + 1;
cwAdjY = pt.y + 1;
angleCW = directionAngles[2][2];
ccwAdjX = pt.x + 1;
ccwAdjY = pt.y;
angleCCW = directionAngles[2][1];
isStraightCCW = false;
break;
//negative x, negative y (higher on screen), closer to y-axis
case 5:
cwAdjX = pt.x + 1;
cwAdjY = pt.y + 1;
angleCW = directionAngles[2][2];
ccwAdjX = pt.x;
ccwAdjY = pt.y + 1;
angleCCW = directionAngles[1][2];
isStraightCCW = true;
break;
//positive x, negative y (higher on screen), closer to y-axis
case 6:
cwAdjX = pt.x;
cwAdjY = pt.y + 1;
angleCW = directionAngles[1][2];
isStraightCCW = false;
ccwAdjX = pt.x - 1;
ccwAdjY = pt.y + 1;
angleCCW = directionAngles[0][2];
break;
//positive x, negative y (higher on screen), closer to x-axis
default:
cwAdjX = pt.x - 1;
cwAdjY = pt.y + 1;
angleCW = directionAngles[0][2];
ccwAdjX = pt.x - 1;
ccwAdjY = pt.y;
angleCCW = directionAngles[0][1];
isStraightCCW = true;
break;
}
/*
angleCCW = (((Math.abs(atan2Cache[ccwAdjX][ccwAdjY] - angleCCW) > Math.PI)
? atan2Cache[ccwAdjX][ccwAdjY] + angleCCW + PI2
: atan2Cache[ccwAdjX][ccwAdjY] + angleCCW)
* 0.5) % PI2;
//(angleCCW + atan2Cache[ccwAdjX][ccwAdjY]) * 0.5;
angleCW = (((Math.abs(atan2Cache[cwAdjX][cwAdjY] - angleCW) > Math.PI)
? atan2Cache[cwAdjX][cwAdjY] + angleCW + PI2
: atan2Cache[cwAdjX][cwAdjY] + angleCW)
* 0.5) % PI2;
//(angleCW + atan2Cache[cwAdjX][cwAdjY]) * 0.5;
* /
angleCCW = atan2Cache[ccwAdjX][ccwAdjY];
//(angleCCW + atan2Cache[ccwAdjX][ccwAdjY]) * 0.5;
angleCW = atan2Cache[cwAdjX][cwAdjY];
//(angleCW + atan2Cache[cwAdjX][cwAdjY]) * 0.5;
cwGridX = cwAdjX + viewerX - maxRadius;
ccwGridX = ccwAdjX + viewerX - maxRadius;
cwGridY = cwAdjY + viewerY - maxRadius;
ccwGridY = ccwAdjY + viewerY - maxRadius;
blockedCW = cwGridX >= width || cwGridY >= height || cwGridX < 0 || cwGridY < 0 ||
resMap[cwGridX][cwGridY] > 0.5 ||
angleMap[cwAdjX][cwAdjY] >= PI2;
blockedCCW = ccwGridX >= width || ccwGridY >= height || ccwGridX < 0 || ccwGridY < 0 ||
resMap[ccwGridX][ccwGridY] > 0.5 ||
angleMap[ccwAdjX][ccwAdjY] >= PI2;
if (blockedCW && blockedCCW) {
angleMap[pt.x][pt.y] = PI2;
continue;
}
if (theta % (HALF_PI - 0.00125) < 0.005)
if (isStraightCCW) {
if (blockedCCW) {
angleMap[pt.x][pt.y] = PI2;
gradientMap[cx][cy] = dist;
continue;
}
else
angleMap[pt.x][pt.y] = theta;
} else {
if (blockedCW) {
angleMap[pt.x][pt.y] = PI2;
gradientMap[cx][cy] = dist;
continue;
}
else
angleMap[pt.x][pt.y] = theta;
}
else if(theta % (QUARTER_PI - 0.0025) < 0.005)
if (isStraightCCW) {
if (blockedCW) {
angleMap[pt.x][pt.y] = PI2;
gradientMap[cx][cy] = dist;
continue;
}
else
angleMap[pt.x][pt.y] = theta;
} else {
if (blockedCCW) {
angleMap[pt.x][pt.y] = PI2;
gradientMap[cx][cy] = dist;
continue;
}
else
angleMap[pt.x][pt.y] = theta;
}
else {
if (blockedCW) {
angleMap[pt.x][pt.y] = angleMap[ccwAdjX][ccwAdjY];
// angleMap[pt.x][pt.y] = Math.max(angleMap[ccwAdjX][ccwAdjY],
// (theta - (angleCCW - theta + PI2) % PI2 * 0.5 + PI2) % PI2);
// (theta - angleCCW > Math.PI)
// ? (theta - (angleCCW - theta + PI2) * 0.5) % PI2
// : theta - (angleCCW - theta + PI2) % PI2 * 0.5;
//angleMap[pt.x][pt.y] = angleCCW;
// (((Math.abs(theta - angleCCW) > Math.PI)
// ? theta + angleCCW + PI2
// : theta + angleCCW)
// * 0.5) % PI2;
//angleMap[cwAdjX - viewerX + maxRadius][cwAdjY - viewerY + maxRadius];
//Math.abs(angleMap[cwAdjX - viewerX + maxRadius][cwAdjY - viewerY + maxRadius] -
//angleMap[pt.x][pt.y] = Math.abs(theta - angleCCW) +
// angleMap[cwAdjX - viewerX + maxRadius][cwAdjY - viewerY + maxRadius];
} else if (blockedCCW) {
angleMap[pt.x][pt.y] = angleMap[cwAdjX][cwAdjY];
// angleMap[pt.x][pt.y] = Math.max(angleMap[cwAdjX][cwAdjY],
// (theta + (theta - angleCW + PI2) % PI2 * 0.5 + PI2) % PI2);
// (angleCW - theta > Math.PI)
// ? theta + (theta - angleCW + PI2) % PI2 * 0.5
// : (theta + (theta - angleCW + PI2) * 0.5) % PI2;
//angleCW;
//angleMap[ccwAdjX - viewerX + maxRadius][ccwAdjY - viewerY + maxRadius];
//angleMap[ccwAdjX - viewerX + maxRadius][ccwAdjY - viewerY + maxRadius]
}
else
{
double cwTemp = angleMap[cwAdjX][cwAdjY], ccwTemp = angleMap[ccwAdjX][ccwAdjY];
if(cwTemp < 0)
cwTemp = (atan2Cache[cwAdjX][cwAdjY]);
if(ccwTemp < 0)
ccwTemp = (atan2Cache[ccwAdjX][ccwAdjY]);
if(cwTemp != atan2Cache[cwAdjX][cwAdjY] &&
ccwTemp != atan2Cache[ccwAdjX][ccwAdjY])
angleMap[pt.x][pt.y] = 0.5 * (cwTemp + ccwTemp);
else if(ccwTemp != atan2Cache[ccwAdjX][ccwAdjY])
angleMap[pt.x][pt.y] = ccwTemp;
else if(cwTemp != atan2Cache[cwAdjX][cwAdjY])
angleMap[pt.x][pt.y] = cwTemp;
else
angleMap[pt.x][pt.y] = theta;
}
/*
else if (!blockedCW)
angleMap[pt.x][pt.y] = (angleMap[cwAdjX][cwAdjY] != atan2Cache[cwAdjX][cwAdjY])
? angleMap[cwAdjX][cwAdjY]
: theta;
else
angleMap[pt.x][pt.y] = (angleMap[ccwAdjX][ccwAdjY] != atan2Cache[ccwAdjX][ccwAdjY])
? angleMap[ccwAdjX][ccwAdjY]
: theta;
* /
}
if(Math.abs(angleMap[pt.x][pt.y] - theta) <= 0.001 || resMap[pt.x][pt.y] > 0.5)
gradientMap[cx][cy] = dist;
else
angleMap[pt.x][pt.y] = PI2 * 2;
}
}
}
}
return gradientMap;
}
*/
public byte[][] waveFOV(int viewerX, int viewerY) {
byte[][] gradientMap = new byte[width][height];
double[][] angleMap = new double[2 * maxRadius + 1][2 * maxRadius + 1];
gradientMap[viewerX][viewerY] = (byte)(1 + maxRadius);
int cx, cy, nearCWx, nearCWy, nearCCWx, nearCCWy;
Coord pt;
double theta, angleCW, angleCCW, straight;
byte dist;
boolean blockedCCW, blockedCW;
for(int w = 0; w < waves.length; w++)
{
for(int c = 0; c < waves[w].length; c++)
{
pt = waves[w][c];
cx = viewerX - maxRadius + pt.x;
cy = viewerY - maxRadius + pt.y;
if(cx < width && cx >= 0 && cy < height && cy >= 0)
{
theta = atan2Cache[pt.x][pt.y];
dist = (byte) distanceCache[pt.x][pt.y ];
if(w <= 0)
{
gradientMap[cx][cy] = dist;
}
else {
switch ((int) Math.floor(theta / QUARTER_PI)) {
//positive x, postive y, closer to x-axis
case 0:
nearCCWx = pt.x - 1;
nearCCWy = pt.y;
angleCCW = directionAngles[0][1]; //atan2Cache[nearCCWx][nearCCWy];
straight = angleCCW;
nearCWx = pt.x - 1;
nearCWy = pt.y - 1;
angleCW = directionAngles[0][0]; //atan2Cache[nearCWx][nearCWy];
break;
//positive x, postive y, closer to y-axis
case 1:
nearCWx = pt.x;
nearCWy = pt.y - 1;
angleCW = directionAngles[1][0];
straight = angleCW;
nearCCWx = pt.x - 1;
nearCCWy = pt.y - 1;
angleCCW = directionAngles[0][0];
break;
//negative x, postive y, closer to y-axis
case 2:
nearCCWx = pt.x;
nearCCWy = pt.y - 1;
angleCCW = directionAngles[1][0];
straight = angleCCW;
nearCWx = pt.x + 1;
nearCWy = pt.y - 1;
angleCW = directionAngles[2][0];
break;
//negative x, postive y, closer to x-axis
case 3:
nearCCWx = pt.x + 1;
nearCCWy = pt.y;
angleCCW = directionAngles[2][1];
straight = angleCCW;
nearCWx = pt.x + 1;
nearCWy = pt.y - 1;
angleCW = directionAngles[2][0];
break;
//negative x, negative y, closer to x-axis
case 4:
nearCWx = pt.x + 1;
nearCWy = pt.y;
angleCW = -directionAngles[2][1];
straight = angleCW;
nearCCWx = pt.x + 1;
nearCCWy = pt.y + 1;
angleCCW = directionAngles[2][2];
break;
//negative x, negative y, closer to y-axis
case 5:
nearCWx = pt.x;
nearCWy = pt.y + 1;
angleCW = directionAngles[1][2];
straight = angleCW;
nearCCWx = pt.x + 1;
nearCCWy = pt.y + 1;
angleCCW = directionAngles[2][2];
break;
//positive x, negative y, closer to y-axis
case 6:
nearCCWx = pt.x;
nearCCWy = pt.y + 1;
angleCCW = directionAngles[1][2];
straight = angleCCW;
nearCWx = pt.x - 1;
nearCWy = pt.y + 1;
angleCW = directionAngles[0][2];
break;
//positive x, negative y, closer to x-axis
default:
nearCWx = pt.x - 1;
nearCWy = pt.y;
angleCW = directionAngles[0][1];
straight = angleCW;
nearCCWx = pt.x - 1;
nearCCWy = pt.y + 1;
angleCCW = directionAngles[0][2];
break;
}
nearCCWx += viewerX - maxRadius;
nearCWx += viewerX - maxRadius;
nearCCWy += viewerY - maxRadius;
nearCWy += viewerY - maxRadius;
blockedCCW = resMap[nearCCWx][nearCCWy] > 0.5 ||
angleMap[nearCCWx - viewerX + maxRadius][nearCCWy - viewerY + maxRadius] >= PI2;
blockedCW = resMap[nearCWx][nearCWy] > 0.5 ||
angleMap[nearCWx - viewerX + maxRadius][nearCWy - viewerY + maxRadius] >= PI2;
if( theta == 0 || theta == Math.PI || (Math.abs(theta) - HALF_PI < 0.005 && Math.abs(theta) - HALF_PI > -0.005))
angleMap[pt.x][pt.y] = (straight == angleCCW)
? (blockedCCW)
? PI2
: angleMap[nearCCWx - viewerX + maxRadius][nearCCWy - viewerY + maxRadius]
: (blockedCW)
? PI2
: angleMap[nearCWx - viewerX + maxRadius][nearCWy - viewerY + maxRadius];
else {
if (blockedCW && blockedCCW) {
angleMap[pt.x][pt.y] = PI2;
continue;
}
if (blockedCW) {
angleMap[pt.x][pt.y] = Math.abs(theta - angleCCW) + SLIVER_PI;
//angleMap[nearCCWx - viewerX + maxRadius][nearCCWy - viewerY + maxRadius];
//Math.abs(angleMap[nearCCWx - viewerX + maxRadius][nearCCWy - viewerY + maxRadius] -
//angleMap[pt.x][pt.y] = Math.abs(theta - angleCCW) +
// angleMap[nearCCWx - viewerX + maxRadius][nearCCWy - viewerY + maxRadius];
} else if (blockedCCW) {
angleMap[pt.x][pt.y] = Math.abs(angleCW - theta) + SLIVER_PI;
//angleMap[nearCWx - viewerX + maxRadius][nearCWy - viewerY + maxRadius];
//angleMap[nearCWx - viewerX + maxRadius][nearCWy - viewerY + maxRadius]
}
if (!blockedCW)
angleMap[pt.x][pt.y] += 0.5 *
angleMap[nearCWx - viewerX + maxRadius][nearCWy - viewerY + maxRadius];
if (!blockedCCW)
angleMap[pt.x][pt.y] += 0.5 *
angleMap[nearCCWx - viewerX + maxRadius][nearCCWy - viewerY + maxRadius];
}
if(angleMap[pt.x][pt.y] <= fovPermissiveness)
gradientMap[cx][cy] = dist;
else
angleMap[pt.x][pt.y] = PI2;
}
}
}
}
return gradientMap;
}
public byte[][] slopeShadowFOV(int viewerX, int viewerY)
{
byte[][] lightMap = new byte[width][height];
lightMap[viewerX][viewerY] = (byte)(1 + maxRadius);
for (Direction d : Direction.DIAGONALS) {
slopeShadowCast(1, 1.0, 0.0, 0, d.deltaX, d.deltaY, 0, viewerX, viewerY, lightMap);
slopeShadowCast(1, 1.0, 0.0, d.deltaX, 0, 0, d.deltaY, viewerX, viewerY, lightMap);
}
return lightMap;
}
private byte[][] slopeShadowCast(int row, double start, double end, int xx, int xy, int yx, int yy,
int viewerX, int viewerY, byte[][] lightMap) {
double newStart = 0;
if (start < end) {
return lightMap;
}
int width = lightMap.length;
int height = lightMap[0].length;
boolean blocked = false;
int dist;
for (int distance = row; distance <= maxRadius && !blocked; distance++) {
int deltaY = -distance;
for (int deltaX = -distance; deltaX <= 0; deltaX++) {
int currentX = viewerX + deltaX * xx + deltaY * xy;
int currentY = viewerY + deltaX * yx + deltaY * yy;
double leftSlope = (deltaX - 0.5f) / (deltaY + 0.5f);
double rightSlope = (deltaX + 0.5f) / (deltaY - 0.5f);
if (!(currentX >= 0 && currentY >= 0 && currentX < width && currentY < height
&& currentX - viewerX + maxRadius >= 0 && currentX - viewerX <= maxRadius
&& currentY - viewerY + maxRadius >= 0 && currentY - viewerY <= maxRadius)
|| start < rightSlope) {
continue;
} else if (end > leftSlope) {
break;
}
dist = distanceCache[currentX - viewerX + maxRadius][currentY - viewerY + maxRadius];
//check if it's within the lightable area and light if needed
if (dist <= maxRadius) {
lightMap[currentX][currentY] = (byte) dist;
}
if (blocked) { //previous cell was a blocking one
if (resMap[currentX][currentY] >= 0.5) {//hit a wall
newStart = rightSlope;
} else {
blocked = false;
start = newStart;
}
} else {
if (resMap[currentX][currentY] >= 0.5 && distance < maxRadius) {//hit a wall within sight line
blocked = true;
lightMap = slopeShadowCast(distance + 1, start, leftSlope, xx, xy, yx, yy, viewerX, viewerY, lightMap);
newStart = rightSlope;
}
}
}
}
return lightMap;
}
public short[][] improveQuality(int viewerX, int viewerY) {
if(!complete) throw new IllegalStateException(
"cacheAllPerformance() must be called before improveQuality() to fill the cache.");
if (viewerX < 0 || viewerY < 0 || viewerX >= width || viewerY >= height)
return ALL_WALLS;
if (resMap[viewerX][viewerY] >= 1.0) {
return ALL_WALLS;
}
short myHilbert = (short)posToHilbert(viewerX, viewerY);
ShortVLA packing = new ShortVLA(128);
short[][] packed = new short[maxRadius + 1][], cached = cache[viewerX + viewerY * width];
short[] losCached = losCache[viewerX + viewerY * width];
///*
//short[] perimeter = allPackedHilbert(fringe(losCached, 2, width, height));
int xr = Math.max(0, viewerX - 1 - maxLOSRadius), yr = Math.max(0, viewerY - 1 - maxLOSRadius),
wr = Math.min(width - 1 - viewerX, maxLOSRadius * 2 + 3),
hr = Math.min(height - 1 - viewerY, maxLOSRadius * 2 + 3);
short[] perimeter = rectangleHilbert(xr, yr, wr, hr);
short p_x, p_y;
for (int i = 0; i < perimeter.length; i++) {
p_x = hilbertX[perimeter[i] & 0xffff];
p_y = hilbertY[perimeter[i] & 0xffff];
if (queryPackedHilbert(losCache[p_x + p_y * width], myHilbert))
packing.add(perimeter[i]);
}
//*/
/*
boolean[][] losUnpacked = unpack(losCached, width, height);
for (int x = Math.max(0, viewerX - 62); x <= Math.min(viewerX + 62, width - 1); x++) {
for (int y = Math.max(0, viewerY - 62); y <= Math.min(viewerY + 62, height - 1); y++) {
if (losUnpacked[x][y])
continue;
if(losCache[x + y * width] == ALL_WALL)
continue;
if (distance(x - viewerX, y - viewerY) / 2 > 62)
continue;
if (queryPackedHilbert(losCache[x + y * width], myHilbert))
packing.add((short) posToHilbert(x, y));
}
}
*/
losCache[viewerX + viewerY * width] = insertSeveralPacked(losCached, packing.asInts());
for (int l = 0; l <= maxRadius; l++) {
packing.clear();
xr = Math.max(0, viewerX - l);
yr = Math.max(0, viewerY - l);
wr = Math.min(width - viewerX + l, l * 2 + 1);
hr = Math.min(height - viewerY + l, l * 2 + 1);
perimeter = rectangleHilbert(xr, yr, wr, hr);
//short p_x, p_y;
for (int i = 0; i < perimeter.length; i++) {
if(queryPackedHilbert(cached[maxRadius - l], perimeter[i])) {
packing.add(perimeter[i]);
continue;
}
p_x = hilbertX[perimeter[i] & 0xffff];
p_y = hilbertY[perimeter[i] & 0xffff];
if(p_y >= height || p_x >= width || cache[p_x + p_y * width] == ALL_WALLS)
continue;
if (distance(p_x - viewerX, p_y - viewerY) >> 1 > l)
continue;
if (queryPackedHilbert(cache[p_x + p_y * width][maxRadius - l], myHilbert))
packing.add(perimeter[i]);
}
/*
knownSeen = cached[maxRadius - l];
for (int x = Math.max(0, viewerX - l); x <= Math.min(viewerX + l, width - 1); x++) {
for (int y = Math.max(0, viewerY - l); y <= Math.min(viewerY + l, height - 1); y++) {
if(cache[x + y * width] == ALL_WALLS)
continue;
if (distance(x - viewerX, y - viewerY) / 2 > l)
continue;
short i = (short) posToHilbert(x, y);
if (queryPackedHilbert(knownSeen, i))
continue;
if (queryPackedHilbert(cache[x + y * width][maxRadius - l], myHilbert))
packing.add(i);
}
}*/
packed[maxRadius - l] = packSeveral(packing.asInts());
Coord light;
for (int i = 0; i < lightSources.length; i++) {
light = lightSources[i];
packed[maxRadius - l] = unionPacked(packed[maxRadius - l], differencePacked(intersectPacked(losCached,
cache[light.x + light.y * width][maxRadius - lightBrightnesses[i]]), wallMap));
}
}
return packed;
}
/**
* Runs FOV calculations on another thread, without interrupting this one. Before using the cache, you should call
* awaitCachePerformance() to ensure this method has finished on its own thread, but be aware that this will cause
* the thread that calls awaitCachePerformance() to essentially freeze until FOV calculations are over.
*/
private void cacheAllPerformance() {
if(performanceThread != null || complete)
return;
performanceThread = new Thread(new PerformanceUnit());
performanceThread.start();
}
/**
* If FOV calculations from cacheAllPerformance() are being done on another thread, calling this method will make
* the current thread wait for the FOV calculations' thread to finish, "freezing" the current thread until it does.
* This ensures the cache will be complete after this method returns true, and if this method returns false, then
* something has gone wrong.
* @return true if cacheAllPerformance() has successfully completed, false otherwise.
*/
@SuppressWarnings("unused")
private boolean awaitCachePerformance()
{
if(performanceThread == null && !complete)
cacheAllPerformance();
if(complete) return true;
try {
performanceThread.join();
} catch (InterruptedException e) {
return false;
}
return complete;
}
/**
* Runs FOV calculations on another thread, without interrupting this one, then performs additional quality tweaks
* and adds any distant lights, if there were any in the constructor. Before using the cache, you should call
* awaitCache() to ensure this method has finished on its own thread, but be aware that this will cause
* the thread that calls awaitCache() to essentially freeze until FOV calculations are over.
*/
public void cacheAll()
{
if(qualityThread != null || qualityComplete)
return;
if(!complete) {
cacheAllPerformance();
try {
performanceThread.join();
} catch (InterruptedException e) {
return;
}
}
qualityThread = new Thread(new QualityUnit());
qualityThread.start();
}
/**
* If FOV calculations from cacheAll() are being done on another thread, calling this method will make
* the current thread wait for the FOV calculations' thread to finish, "freezing" the current thread until it does.
* This ensures the cache will be complete with the additional quality improvements such as distant lights after
* this method returns true, and if this method returns false, then something has gone wrong.
* @return true if cacheAll() has successfully completed, false otherwise.
*/
public boolean awaitCache()
{
if(performanceThread == null || !complete || (qualityThread == null && !qualityComplete))
cacheAll();
if(qualityComplete) return true;
try {
qualityThread.join();
} catch (Exception e) {
return false;
}
return qualityComplete;
}
/**
* Runs FOV calculations for any cells that were changed as a result of newMap being different from the map passed
* to the FOVCache constructor. It runs these on another thread, without interrupting this one. Before using the
* cache, you should call awaitRefresh() to ensure this method has finished on its own thread, but be aware that
* this will cause the thread that calls awaitRefresh() to essentially freeze until FOV calculations are over.
*/
public void refreshCache(char[][] newMap)
{
performanceThread = new Thread(new RefreshUnit(newMap));
performanceThread.start();
}
/**
* If FOV calculations from refreshCache() are being done on another thread, calling this method will make
* the current thread wait for the FOV calculations' thread to finish, "freezing" the current thread until it does.
* This ensures the cache will be complete with the updates from things like opening a door after
* this method returns true, and if this method returns false, then something has gone wrong.
* @return true if refreshCache() has successfully completed, false otherwise.
*/
public boolean awaitRefresh(char[][] newMap)
{
if(!performanceThread.isAlive() && !refreshComplete)
refreshCache(newMap);
if(refreshComplete) return true;
try {
performanceThread.join();
} catch (InterruptedException e) {
return false;
}
if(refreshComplete)
{
refreshComplete = false;
return true;
}
else return false;
}
@SuppressWarnings("unused")
private byte heuristic(Direction target) {
switch (radiusKind) {
case CIRCLE:
case SPHERE:
switch (target) {
case DOWN_LEFT:
case DOWN_RIGHT:
case UP_LEFT:
case UP_RIGHT:
return 3;
default:
return 2;
}
default:
return 2;
}
}
private short distance(int xPos, int yPos) {
int x = Math.abs(xPos), y = Math.abs(yPos);
switch (radiusKind) {
case CIRCLE:
case SPHERE:
{
if(x == y)
return (short)(3 * x);
else if(x < y)
return (short)(3 * x + 2 * (y - x));
else
return (short)(3 * y + 2 * (x - y));
}
case DIAMOND:
case OCTAHEDRON:
return (short)(2 * (x + y));
default:
return (short)(2 * Math.max(x, y));
}
}
/**
* Calculates the Field Of View for the provided map from the given x, y
* coordinates. Returns a light map where the values are either 1.0 or 0.0.
* Takes a double[][] resistance map that will be disregarded.
*
* The starting point for the calculation is considered to be at the center
* of the origin cell. Radius determinations are based on the radiusKind given
* in construction. The light will be treated as having radius 62, regardless
* of the maxRadius passed to the constructor; this should in most cases be
* suitable when limitless light is desired.
* If the cache has not been fully constructed, this will compute a new FOV
* map using Shadow FOV instead of using the cache, and the result will not
* be cached.
*
* @param resistanceMap the grid of cells to calculate on
* @param startx the horizontal component of the starting location
* @param starty the vertical component of the starting location
* @return the computed light grid
*/
@Override
public double[][] calculateFOV(double[][] resistanceMap, int startx, int starty) {
if(qualityComplete || complete)
return unpackDouble(losCache[startx + starty * width], width, height);
else
return fov.calculateFOV(resMap, startx, starty, maxRadius, radiusKind);
}
/*
* Calculates the Field Of View for the provided map from the given x, y
* coordinates. Returns a light map where the values are either 1.0 or 0.0.
* Takes a double radius to extend out to (rounded to the nearest int) and
* a double[][] resistance map that will be disregarded.
*
* The starting point for the calculation is considered to be at the center
* of the origin cell. Radius determinations are based on the radiusKind given
* in construction. The light will be treated as having the given radius, but
* if that is higher than the maximum possible radius stored by this FOVCache,
* it will compute a new FOV map using Shadow FOV instead of using the cache.
* If the cache has not been fully constructed, this will compute a new FOV
* map using Shadow FOV instead of using the cache, and the result will not
* be cached.
*
* @param resistanceMap the grid of cells to calculate on
* @param startx the horizontal component of the starting location
* @param starty the vertical component of the starting location
* @param radius the distance the light will extend to
* @return the computed light grid
*/
/*
@Override
public double[][] calculateFOV(double[][] resistanceMap, int startx, int starty, double radius) {
if((qualityComplete || complete) && radius >= 0 && radius <= maxRadius)
return unpackDouble(cache[startx + starty * width][maxRadius - (int) Math.round(radius)], width, height);
else
return fov.calculateFOV(resMap, startx, starty, radius, radiusKind);
}*/
/*
* Calculates the Field Of View for the provided map from the given x, y
* coordinates. Returns a light map where the values are either 1.0 or 0.0.
* Takes a double radius to extend out to (rounded to the nearest int), a
* Radius enum that should match the Radius this FOVCache was constructed
* with, and a double[][] resistance map that will be disregarded.
*
* The starting point for the calculation is considered to be at the center
* of the origin cell. Radius determinations are based on the radiusTechnique
* passed to this method, but will only use the cache if the Radius given is
* the same kind as the one given in construction. The light will be treated
* as having the given radius, but if that is higher than the maximum possible
* radius stored by this FOVCache, it will compute a new FOV map using Shadow
* FOV instead of using the cache.
* If the cache has not been fully constructed, this will compute a new FOV
* map using Shadow FOV instead of using the cache, and the result will not
* be cached.
*
* @param resistanceMap the grid of cells to calculate on
* @param startX the horizontal component of the starting location
* @param startY the vertical component of the starting location
* @param radius the distance the light will extend to
* @param radiusTechnique provides a means to calculate the radius as desired
* @return the computed light grid
*/
/*
@Override
public double[][] calculateFOV(double[][] resistanceMap, int startX, int startY, double radius,
Radius radiusTechnique) {
if((qualityComplete || complete) && radius >= 0 && radius <= maxRadius &&
radiusKind.equals2D(radiusTechnique))
return unpackDouble(cache[startX + startY * width][maxRadius - (int) Math.round(radius)], width, height);
else
return fov.calculateFOV(resMap, startX, startY, radius, radiusTechnique);
}*/
/*
* Calculates the conical Field Of View for the provided map from the given
* x, y coordinates. Returns a light map where the values are either 1.0 or
* 0.0. Takes a double radius to extend out to (rounded to the nearest int),
* a Radius enum that should match the Radius this FOVCache was constructed
* with, a double[][] resistance map that will be disregarded, an angle to
* center the conical FOV on (in degrees), and the total span in degrees
* for the FOV to cover.
*
* The starting point for the calculation is considered to be at the center
* of the origin cell. Radius determinations are based on the radiusTechnique
* passed to this method, but will only use the cache if the Radius given is
* the same kind as the one given in construction. The light will be treated
* as having the given radius, but if that is higher than the maximum possible
* radius stored by this FOVCache, it will compute a new FOV map using Shadow
* FOV instead of using the cache. A conical section of FOV is lit by this
* method if span is greater than 0.
*
* If the cache has not been fully constructed, this will compute a new FOV
* map using Shadow FOV instead of using the cache, and the result will not
* be cached.
*
* @param resistanceMap the grid of cells to calculate on
* @param startX the horizontal component of the starting location
* @param startY the vertical component of the starting location
* @param radius the distance the light will extend to
* @param radiusTechnique provides a means to calculate the radius as desired
* @param angle the angle in degrees that will be the center of the FOV cone, 0 points right
* @param span the angle in degrees that measures the full arc contained in the FOV cone
* @return the computed light grid
*/
/* @Override
public double[][] calculateFOV(double[][] resistanceMap, int startX, int startY, double radius,
Radius radiusTechnique, double angle, double span) {
if((qualityComplete || complete) && radius >= 0 && radius <= maxRadius &&
radiusKind.equals2D(radiusTechnique))
return unpackDoubleConical(cache[startX + startY * width][maxRadius - (int) Math.round(radius)], width, height,
startX, startY, angle, span);
else
return fov.calculateFOV(resMap, startX, startY, radius, radiusTechnique, angle, span);
}
*/
/**
* Calculates the Field Of View for the provided map from the given x, y
* coordinates. Returns a light map where the values range from 1.0 (center
* of the FOV) to 0.0 (not seen), with values between those two extremes
* for the rest of the seen area.
* Takes a double radius to extend out to (rounded to the nearest int), and
* a double[][] resistance map that will be disregarded.
*
* The starting point for the calculation is considered to be at the center
* of the origin cell. Radius determinations are based on the radiusKind given
* in construction. The light will be treated as having the given radius, but
* if that is higher than the maximum possible radius stored by this FOVCache,
* it will compute a new FOV map using Ripple FOV instead of using the cache.
* If the cache has not been fully constructed, this will compute a new FOV
* map using Ripple FOV instead of using the cache, and the result will not
* be cached.
*
* @param resistanceMap the grid of cells to calculate on
* @param startx the horizontal component of the starting location
* @param starty the vertical component of the starting location
* @param radius the distance the light will extend to
* @return the computed light grid
*/
@Override
public double[][] calculateFOV(double[][] resistanceMap, int startx, int starty, double radius) {
if((qualityComplete || complete) && radius > 0 && radius <= maxRadius)
return unpackMultiDoublePartial(cache[startx + starty * width], width, height,
levels[(int) Math.round(radius)], (int) Math.round(radius));
else
return gradedFOV.calculateFOV(resMap, startx, starty, radius, radiusKind);
}
/**
* Calculates the Field Of View for the provided map from the given x, y
* coordinates. Returns a light map where the values range from 1.0 (center
* of the FOV) to 0.0 (not seen), with values between those two extremes
* for the rest of the seen area.
* Takes a double radius to extend out to (rounded to the nearest int), a
* Radius enum that should match the Radius this FOVCache was constructed
* with, and a double[][] resistance map that will be disregarded.
*
* The starting point for the calculation is considered to be at the center
* of the origin cell. Radius determinations are based on the radiusTechnique
* passed to this method, but will only use the cache if the Radius given is
* the same kind as the one given in construction. The light will be treated
* as having the given radius, but if that is higher than the maximum possible
* radius stored by this FOVCache, it will compute a new FOV map using Ripple
* FOV instead of using the cache.
* If the cache has not been fully constructed, this will compute a new FOV
* map using Ripple FOV instead of using the cache, and the result will not
* be cached.
*
* @param resistanceMap the grid of cells to calculate on
* @param startX the horizontal component of the starting location
* @param startY the vertical component of the starting location
* @param radius the distance the light will extend to
* @param radiusTechnique provides a means to calculate the radius as desired
* @return the computed light grid
*/
@Override
public double[][] calculateFOV(double[][] resistanceMap, int startX, int startY, double radius,
Radius radiusTechnique) {
if((qualityComplete || complete) && radius > 0 && radius <= maxRadius &&
radiusKind.equals2D(radiusTechnique))
return unpackMultiDoublePartial(cache[startX + startY * width], width, height,
levels[(int) Math.round(radius)], (int) Math.round(radius));
else
return gradedFOV.calculateFOV(resMap, startX, startY, radius, radiusTechnique);
}
/**
* Calculates the conical Field Of View for the provided map from the given
* x, y coordinates. Returns a light map where the values range from 1.0
* (center of the FOV) to 0.0 (not seen), with values between those two
* extremes for the rest of the seen area.
* Takes a double radius to extend out to (rounded to the nearest int), a
* Radius enum that should match the Radius this FOVCache was constructed
* with, a double[][] resistance map that will be disregarded, an angle to
* center the conical FOV on (in degrees), and the total span in degrees
* for the FOV to cover.
*
* The starting point for the calculation is considered to be at the center
* of the origin cell. Radius determinations are based on the radiusTechnique
* passed to this method, but will only use the cache if the Radius given is
* the same kind as the one given in construction. The light will be treated
* as having the given radius, but if that is higher than the maximum possible
* radius stored by this FOVCache, it will compute a new FOV map using Ripple
* FOV instead of using the cache. A conical section of FOV is lit by this
* method if span is greater than 0.
*
* If the cache has not been fully constructed, this will compute a new FOV
* map using Ripple FOV instead of using the cache, and the result will not
* be cached.
*
* @param resistanceMap the grid of cells to calculate on
* @param startX the horizontal component of the starting location
* @param startY the vertical component of the starting location
* @param radius the distance the light will extend to
* @param radiusTechnique provides a means to calculate the radius as desired
* @param angle the angle in degrees that will be the center of the FOV cone, 0 points right
* @param span the angle in degrees that measures the full arc contained in the FOV cone
* @return the computed light grid
*/
@Override
public double[][] calculateFOV(double[][] resistanceMap, int startX, int startY, double radius,
Radius radiusTechnique, double angle, double span) {
if((qualityComplete || complete) && radius > 0 && radius <= maxRadius &&
radiusKind.equals2D(radiusTechnique))
return unpackMultiDoublePartialConical(cache[startX + startY * width], width, height,
levels[(int) Math.round(radius)], (int) Math.round(radius), startX, startY, angle, span);
else
return gradedFOV.calculateFOV(resMap, startX, startY, radius, radiusTechnique, angle, span);
}
/**
* Calculates an array of Coord positions that can be seen along the line from the given start point and end point.
* Does not order the array. Uses the pre-computed LOS cache to determine obstacles, and tends to draw a
* thicker line than Bresenham lines will. This uses the same radiusKind the FOVCache was created with, but the line
* this draws doesn't necessarily travel along valid directions for creatures (in particular, Radius.DIAMOND should
* only allow orthogonal movement, but if you request a 45-degree line, the LOS will have Coords on a perfect
* diagonal, though they won't travel through walls that occupy a thin perpendicular diagonal).
* @param startX the x position of the starting point; must be within bounds of the map
* @param startY the y position of the starting point; must be within bounds of the map
* @param endX the x position of the endpoint; does not need to be within bounds (will stop LOS at the edge)
* @param endY the y position of the endpoint; does not need to be within bounds (will stop LOS at the edge)
* @return a Coord[], unordered, that can be seen along the line of sight; limited to maxLOSRadius, default 62
*/
public Coord[] calculateLOS(int startX, int startY, int endX, int endY)
{
if(!complete || startX < 0 || startX >= width || startY < 0 || startY >= height)
return new Coord[0];
int max = distance(endX - startX, endY - startY);
ArrayList path = new ArrayList<>(max / 2 + 1);
short[] losCached = losCache[startX + startY * width];
if(losCached.length == 0)
return new Coord[0];
boolean on = false;
int idx = 0, xt, yt;
short x =0, y = 0;
path.add(Coord.get(startX, startY));
if(startX == endX && startY == endY)
return path.toArray(new Coord[0]);
double angle = NumberTools.atan2(endY - startY, endX - startX);
double x2 = Math.sin(angle) * 0.5, y2 = Math.cos(angle) * 0.5;
boolean[][] mask = new boolean[width][height];
for (int d = 2; d <= max; d++) {
xt = startX + (int)(x2 * d + 0.5);
yt = startY + (int)(y2 * d + 0.5);
if(xt < 0 || xt >= width || yt < 0 || yt >= height)
break;
mask[xt][yt] = true;
}
for(int p = 0; p < losCached.length; p++, on = !on) {
if (on) {
for (int toSkip = idx +(losCached[p] & 0xffff); idx < toSkip && idx < limit; idx++) {
x = hilbertX[idx];
y = hilbertY[idx];
// this should never be possible, removing tentatively
//if(x >= width || y >= height)
// continue;
if(mask[x][y])
path.add(Coord.get(x, y));
}
} else {
idx += losCached[p] & 0xffff;
}
}
return path.toArray(new Coord[0]);
}
/**
* Calculates an array of Coord positions that can be seen along the line from the given start point and end point.
* Sorts the array, starting from the closest Coord to start and ending close to end. Uses the pre-computed LOS
* cache to determine obstacles, and tends to draw a thicker line than Bresenham lines will. This uses the same
* radiusKind the FOVCache was created with, but the line this draws doesn't necessarily travel along valid
* directions for creatures (in particular, Radius.DIAMOND should only allow orthogonal movement, but if you request
* a 45-degree line, the LOS will have Coords on a perfect diagonal, though they won't travel through walls that
* occupy a thin perpendicular diagonal).
* @param startX the x position of the starting point; must be within bounds of the map
* @param startY the y position of the starting point; must be within bounds of the map
* @param endX the x position of the endpoint; does not need to be within bounds (will stop LOS at the edge)
* @param endY the y position of the endpoint; does not need to be within bounds (will stop LOS at the edge)
* @return a Coord[], sorted, that can be seen along the line of sight; limited to max LOS range, 62
*/
public Coord[] sortedLOS(int startX, int startY, int endX, int endY) {
Coord[] path = calculateLOS(startX, startY, endX, endY);
SortedMap sorted = new TreeMap<>();
double modifier = 0.0001;
Coord c;
for (int i = 0; i < path.length; i++, modifier += 0.0001) {
c = path[i];
sorted.put(distance(c.x, c.y) + modifier, c);
}
return sorted.values().toArray(new Coord[sorted.size()]);
}
/**
* Given a path as a List of Coords (such as one produced by DijkstraMap.getPath()), this method will look up the
* FOV for the given fovRange at each Coord, and returns an array of packed FOV maps where each map is the union
* of the FOV centered on a Coord in path with all FOVs centered on previous Coords in path. The purpose of this is
* mainly to have an efficient way to show the progressively expanding seen area of a character who moves multiple
* tiles. It may be desirable to add the entire path's cumulative FOV (stored in the last element of the returned
* short[][]) to the history of what a character has seen, removing the path's FOV from the currently visible cells
* either after the character has finished their action, or even immediately after moving if, for instance, the
* movement was part of a rapid leap that wouldn't let the character spot details while moving (but the general
* layout provided by the seen-cell history could suffice). This method never unpacks any packed data.
* @param path a List of Coords that will be added, in order, to multiple packed FOVs for the path so far
* @param fovRange the radius the creature or thing taking the path can see (or possibly light up).
* @return a packed short[][], each short[] encoding the FOV around an additional Coord merged with those before
*/
public short[][] pathFOVPacked(List path, int fovRange)
{
if(!complete)
throw new IllegalStateException("Cache is not yet constructed");
if(fovRange > maxRadius)
throw new UnsupportedOperationException("Given fovRange parameter exceeds maximum cached range");
short[][] fovSteps = new short[path.size()][];
int idx = 0;
for (Coord c : path)
{
if(c.x < 0 || c.y < 0 || c.x >= width || c.y >= height)
throw new ArrayIndexOutOfBoundsException("Along given path, encountered an invalid Coord: "
+ c);
if(idx == 0)
{
fovSteps[idx] = cache[c.x + c.y * width][maxRadius - fovRange];
}
else
{
fovSteps[idx] = unionPacked(fovSteps[idx - 1], cache[c.x + c.y * width][maxRadius - fovRange]);
}
idx++;
}
return fovSteps;
}
/**
* Given a path as a List of Coords (such as one produced by DijkstraMap.getPath()), this method will look up the
* FOV for the given fovRange at each Coord, and returns an array of full FOV maps where each map is the union
* of the FOV centered on a Coord in path with all FOVs centered on previous Coords in path. The purpose of this is
* mainly to have a way to show the progressively expanding seen area of a character who moves multiple
* tiles. It may be desirable to add the entire path's cumulative FOV (stored in the last element of the returned
* double[][][]) to the history of what a character has seen, removing the path's FOV from the currently visible
* cells either after the character has finished their action, or even immediately after moving if, for instance,
* the movement was part of a rapid leap that wouldn't let the character spot details while moving (but the general
* layout provided by the seen-cell history could suffice). This method computes the union of the FOV without
* unpacking, but then unpacks each step along the path into a double[][] of 1.0 and 0.0 values.
* @param path a List of Coords that will be added, in order, to multiple FOVs for the path so far
* @param fovRange the radius the creature or thing taking the path can see (or possibly light up).
* @return a packed double[][][]; each double[][] is the FOV around an additional Coord merged with those before
*/
public double[][][] pathFOV(List path, int fovRange)
{
short[][] compressed = pathFOVPacked(path, fovRange);
double[][][] fovSteps = new double[compressed.length][][];
for (int i = 0; i < compressed.length; i++) {
fovSteps[i] = unpackDouble(compressed[i], width, height);
}
return fovSteps;
}
/**
* In games that have multiple characters who should share one FOV map, this method should provide optimal
* performance when collecting several cached FOV maps into one packed map. It takes a Map of Coord keys to Integer
* values, and since it does not modify its parameter, nor does it need a particular iteration order, it doesn't
* perform a defensive copy of the team parameter. Each Coord key should correspond to the position of a character,
* and each Integer value should be the FOV range of that character. This returns a short[] as a packed FOV map for
* all characters in team as a collective.
* @param team a Map of Coord keys for characters' positions to Integer values for the FOV range of each character
* @return a packed FOV map that can be used with other packed data using CoordPacker.
*/
public short[] teamFOVPacked(Map team)
{
if(!complete)
throw new IllegalStateException("Cache is not yet constructed");
short[] packing = new short[0];
int idx = 0;
Coord c;
int range;
for (Map.Entry kv : team.entrySet())
{
c = kv.getKey();
range = kv.getValue();
if(c.x < 0 || c.y < 0 || c.x >= width || c.y >= height)
throw new ArrayIndexOutOfBoundsException("Among team, encountered an invalid Coord: "
+ c);
if(idx == 0)
{
packing = cache[c.x + c.y * width][maxRadius - range];
}
else
{
packing = unionPacked(packing, cache[c.x + c.y * width][maxRadius - range]);
}
idx++;
}
return packing;
}
/**
* In games that have multiple characters who should share one FOV map, this method should provide optimal
* performance when collecting several cached FOV maps into one full map. It takes a Map of Coord keys to Integer
* values, and since it does not modify its parameter, nor does it need a particular iteration order, it doesn't
* perform a defensive copy of the team parameter. Each Coord key should correspond to the position of a character,
* and each Integer value should be the FOV range of that character. This returns a double[][] as a full FOV map,
* with values of either 1.0 or 0.0, for all characters in team as a collective.
* @param team a Map of Coord keys for characters' positions to Integer values for the FOV range of each character
* @return a double[][] FOV map with 1.0 and 0.0 as values, combining all characters' FOV maps.
*/
public double[][] teamFOV(Map team)
{
return unpackDouble(teamFOVPacked(team), width, height);
}
/**
* When you have some packed on/off data and want to make sure it does not include walls, you can use this. It does
* not need to unpack the given packed short[] to get the subset of it without walls. Of course, this needs the
* FOVCache to be
* @param packed a short[] produced by some CoordPacker method or obtained by getLOSEntry() or getCacheEntry().
* @return another packed array containing only the non-wall "on" cells in packed
*/
public short[] removeWalls(short[] packed)
{
return differencePacked(packed, wallMap);
}
public int getMaxRadius() {
return maxRadius;
}
public Radius getRadiusKind() {
return radiusKind;
}
public int getWidth() {
return width;
}
public int getHeight() {
return height;
}
@GwtIncompatible
protected class PerformanceUnit implements Runnable
{
/**
* When an object implementing interface Runnable
is used
* to create a thread, starting the thread causes the object's
* run
method to be called in that separately executing
* thread.
*
* The general contract of the method run
is that it may
* take any action whatsoever.
*
* @see Thread#run()
*/
@Override
public void run() {
ArrayList> losUnits = new ArrayList<>(24);
ArrayList> fovUnits = new ArrayList<>(24);
for (int p = 0; p < 24; p++) {
losUnits.add(new ArrayList(mapLimit / 20));
fovUnits.add(new ArrayList(mapLimit / 20));
}
for (int i = 0, p = 0; i < mapLimit; i++, p = (p+1) % 24) {
losUnits.get(p).add(new LOSUnit(i));
fovUnits.get(p).add(new FOVUnit(i));
}
//long totalTime = System.currentTimeMillis(), threadTime = 0L;
for (int p = 23; p >= 0; p--) {
try {
final List> invoke = executor.invokeAll(losUnits.get(p));
for (Future future : invoke) {
future.get();
future.cancel(false);
//long t = future.get();
//threadTime += t;
//System.out.println(t);
}
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
losUnits.remove(p);
}
for (int p = 23; p >= 0; p--) {
try {
final List> invoke = executor.invokeAll(fovUnits.get(p));
for (Future future : invoke) {
future.get();
future.cancel(false);
//long t = future.get();
//threadTime += t;
//System.out.println(t);
}
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
fovUnits.remove(p);
}
//totalTime = System.currentTimeMillis() - totalTime;
//System.out.println("Total real time elapsed: " + totalTime);
//System.out.println("Total CPU time elapsed, on " + NUM_THREADS + " threads: " + threadTime);
/*
long totalRAM = 0;
for (int c = 0; c < width * height; c++) {
long ctr = 0, losCtr = 0;
for (int i = 0; i < cache[c].length; i++) {
ctr += (((2 * cache[c][i].length + 12 - 1) / 8) + 1) * 8L;
}
totalRAM += (((ctr + 12 - 1) / 8) + 1) * 8;
losCtr = (((2 * losCache[c].length + 12 - 1) / 8) + 1) * 8L;
totalRAM += (((losCtr + 12 - 1) / 8) + 1) * 8;
}
System.out.println("Total memory used by cache: " + totalRAM);
*/
complete = true;
}
}
@GwtIncompatible
protected class QualityUnit implements Runnable
{
/**
* When an object implementing interface Runnable
is used
* to create a thread, starting the thread causes the object's
* run
method to be called in that separately executing
* thread.
*
* The general contract of the method run
is that it may
* take any action whatsoever.
*
* @see Thread#run()
*/
@Override
public void run() {
//long totalTime = System.currentTimeMillis(), threadTime = 0L;
ArrayList> symUnits = new ArrayList<>(4);
for (int p = 0; p < 4; p++) {
symUnits.add(new ArrayList(mapLimit / 3));
}
for (int i = 0, p = 0; i < mapLimit; i++, p = (p+1) % 4) {
symUnits.get(p).add(new SymmetryUnit(i));
}
for (int p = 3; p >= 0; p--) {
try {
final List> invoke = executor.invokeAll(symUnits.get(p));
for (Future future : invoke) {
//threadTime +=
future.get();
future.cancel(false);
//System.out.println(t);
}
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
symUnits.remove(p);
}
cache = tmpCache;
qualityComplete = true;
/*
totalTime = System.currentTimeMillis() - totalTime;
System.out.println("Total real time elapsed : " + totalTime);
System.out.println("Total CPU time elapsed, on " + NUM_THREADS + " threads: " + threadTime);
long totalRAM = 0;
for (int c = 0; c < width * height; c++) {
long ctr = 0, losCtr = 0;
for (int i = 0; i < cache[c].length; i++) {
ctr += (((2 * cache[c][i].length + 12 - 1) / 8) + 1) * 8L;
}
totalRAM += (((ctr + 12 - 1) / 8) + 1) * 8;
losCtr = (((2 * losCache[c].length + 12 - 1) / 8) + 1) * 8L;
totalRAM += (((losCtr + 12 - 1) / 8) + 1) * 8;
}
System.out.println("Total memory used by cache: " + totalRAM);
System.out.println("FOV Map stored for every cell, booleans or bytes, "+width+"x"+height+": " +
((((((((((((height + 12 - 1) / 8) + 1) * 8L * width + 12 - 1) / 8) + 1) * 8L
* height + 12 - 1) / 8) + 1) * 8L * width + 12 - 1) / 8) + 1) * 8L);
*/
}
}
@GwtIncompatible
protected class RefreshUnit implements Runnable
{
protected double[][] res;
protected RefreshUnit(char[][] newMap)
{
res = DungeonUtility.generateResistances(newMap);
}
/**
* When an object implementing interface Runnable
is used
* to create a thread, starting the thread causes the object's
* run
method to be called in that separately executing
* thread.
*
* The general contract of the method run
is that it may
* take any action whatsoever.
*
* @see Thread#run()
*/
@Override
public void run() {
System.arraycopy(cache, 0, tmpCache, 0, tmpCache.length);
short[] needsChange = new short[0];
for (int i = 0; i < width; i++) {
for (int j = 0; j < height; j++) {
if(resMap[i][j] != res[i][j])
{
needsChange = unionPacked(needsChange, losCache[i + j * width]);
}
}
}
needsChange = differencePacked(needsChange, wallMap);
resMap = res;
Coord[] changingCoords = allPacked(needsChange);
List losUnits = new ArrayList<>(changingCoords.length);
List fovUnits = new ArrayList<>(changingCoords.length);
List symUnits = new ArrayList<>(changingCoords.length);
for (int i = 0, idx; i < changingCoords.length; i++)
{
idx = changingCoords[i].x + changingCoords[i].y * width;
losUnits.add(new LOSUnit(idx));
fovUnits.add(new FOVUnit(idx));
symUnits.add(new SymmetryUnit(idx));
}
try {
final List> invoke = executor.invokeAll(losUnits);
for (Future future : invoke) {
//threadTime +=
future.get();
future.cancel(false);
//System.out.println(t);
}
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
try {
final List> invoke = executor.invokeAll(fovUnits);
for (Future future : invoke) {
//threadTime +=
future.get();
future.cancel(false);
//System.out.println(t);
}
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
try {
final List> invoke = executor.invokeAll(symUnits);
for (Future future : invoke) {
//threadTime +=
future.get();
future.cancel(false);
//System.out.println(t);
}
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
cache = tmpCache;
refreshComplete = true;
}
}
@GwtIncompatible
protected class FOVUnit implements Callable
{
protected int index;
public FOVUnit(int index)
{
this.index = index;
}
/**
* Computes a result, or throws an exception if unable to do so.
*
* @return computed result
* @throws Exception if unable to compute a result
*/
@Override
public Long call() throws Exception {
return storeCellFOV(index);
}
}
@GwtIncompatible
protected class LOSUnit implements Callable
{
protected int index;
public LOSUnit(int index)
{
this.index = index;
}
/**
* Computes a result, or throws an exception if unable to do so.
*
* @return computed result
* @throws Exception if unable to compute a result
*/
@Override
public Long call() throws Exception {
return storeCellLOS(index);
}
}
@GwtIncompatible
protected class SymmetryUnit implements Callable
{
protected int index;
public SymmetryUnit(int index)
{
this.index = index;
}
/**
* Computes a result, or throws an exception if unable to do so.
*
* @return computed result
* @throws Exception if unable to compute a result
*/
@Override
public Long call() throws Exception {
return storeCellSymmetry(index);
}
}
/**
* Shuts down any threads that may prevent the game from closing properly.
* It is recommended you call this at the end of the program to avoid threads lingering too long.
* You don't have to do anything special after you call this, other than not using this FOVCache any more.
*/
public void destroy()
{
performanceThread.interrupt();
qualityThread.interrupt();
executor.shutdown();
}
}