All Downloads are FREE. Search and download functionalities are using the official Maven repository.

squidpony.squidgrid.LOS Maven / Gradle / Ivy

Go to download

SquidLib platform-independent logic and utility code. Please refer to https://github.com/SquidPony/SquidLib .

There is a newer version: 3.0.6
Show newest version
package squidpony.squidgrid;

import squidpony.squidmath.*;

import java.io.Serializable;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.List;

/**
 * Line of Sight (LOS) algorithms find if there is or is not a path between two
 * given points.
 * 
* The line found between two points will end at either the target, the * obstruction closest to the start, or the edge of the map. *
* For normal line of sight usage, you should prefer Bresenham lines, and these * are the default (they can also be specified by passing {@link #BRESENHAM} to * the constructor). For more specialized usage, there are other kinds of LOS in * this class, like lines that make no diagonal moves between cells (using * {@link #ORTHO}, or lines that check a wide path (but these use different * methods, like {@link #thickReachable(Radius)}). *
* Performance-wise, all of these methods are rather fast and about the same speed. * {@link #RAY} is a tiny fraction faster than {@link #BRESENHAM} but produces * rather low-quality lines in comparison. Calculating the visibility of 40,000 * lines in a 102x102 dungeon takes within 3% of 950ms (on an Intel i7-4700MQ laptop * processor) for every one of BRESENHAM, DDA, ORTHO, and RAY, even with ORTHO * finding a different kind of line by design. * * @author Eben Howard - http://squidpony.com - [email protected] * @author Tommy Ettinger Added DDA, ORTHO, and the thick lines; some cleanup * @author smelC optimized several methods */ public class LOS implements Serializable { private static final long serialVersionUID = 1L; //constants to indicate desired type of solving algorithm to use /** * A Bresenham-based line-of-sight algorithm. */ public static final int BRESENHAM = 1; /** * Uses Wu's Algorithm as modified by Elias to draw the line. Does * not end at an obstruction but rather returns one of the possible * attempted paths in full. */ public static final int ELIAS = 2; /** * Uses a series of rays internal to the start and end point to * determine visibility. Appearance is extremely close to DDA, which * is also probably a faster algorithm, so BRESENHAM (which can look * a little better) and DDA are recommended instead of RAY. */ public static final int RAY = 3; /** * Draws a line using only North/South/East/West movement. */ public static final int ORTHO = 4; /** * Optimized algorithm for Bresenham-like lines. There are slight * differences in many parts of the lines this draws when compared * to Bresenham lines, but it may also perform significantly better, * and may also be useful as a building block for more complex LOS. * Virtually identical in results to RAY, and just a hair slower, but * better-tested and more predictable. */ public static final int DDA = 5; /** * Draws a line as if with a thick brush, going from a point between * a corner of the starting cell and the center of the starting cell * to the corresponding corner of the target cell, and considers the * target visible if any portion of the thick stroke reached it. Will * result in 1-width lines for exactly-orthogonal or exactly-diagonal * lines and some parts of other lines, but usually is 2 cells wide. */ public static final int THICK = 6; private ArrayDeque lastPath = new ArrayDeque<>(); private int type; private double[][] resistanceMap; private int startx, starty, targetx, targety; private Elias elias = null; private LOS los1 = null, los2 = null; /** * Gets the radius strategy this uses. * @return the current Radius enum used to measure distance; starts at CIRCLE if not specified */ public Radius getRadiusStrategy() { return radiusStrategy; } /** * Set the radius strategy to the given Radius; the default is CIRCLE if this is not called. * @param radiusStrategy a Radius enum to determine how distances are measured */ public void setRadiusStrategy(Radius radiusStrategy) { this.radiusStrategy = radiusStrategy; } private Radius radiusStrategy = Radius.CIRCLE; /** * Constructs an LOS that will draw Bresenham lines and measure distances using the CIRCLE radius strategy. */ public LOS() { this(BRESENHAM); } /** * Constructs an LOS with the given type number, which must equal a static field in this class such as BRESENHAM. * @param type an int that must correspond to the value of a static field in this class (such as BRESENHAM) */ public LOS(int type) { this.type = type; if(type == ELIAS) { elias = new Elias(); los1 = new LOS(BRESENHAM); los2 = new LOS(BRESENHAM); } } /** * Returns true if a line can be drawn from the start point to the target * point without intervening obstructions. * * Uses RadiusStrategy.CIRCLE, or whatever RadiusStrategy was set with setRadiusStrategy . * * @param walls '#' is fully opaque, anything else is fully transparent, as always this uses x,y indexing. * @param startx starting x position on the grid * @param starty starting y position on the grid * @param targetx ending x position on the grid * @param targety ending y position on the grid * @return true if a line can be drawn without being obstructed, false otherwise */ public boolean isReachable(char[][] walls, int startx, int starty, int targetx, int targety) { if(walls.length < 1) return false; double[][] resMap = new double[walls.length][walls[0].length]; for(int x = 0; x < walls.length; x++) { for(int y = 0; y < walls[0].length; y++) { resMap[x][y] = (walls[x][y] == '#') ? 1.0 : 0.0; } } return isReachable(resMap, startx, starty, targetx, targety, radiusStrategy); } /** * Returns true if a line can be drawn from the start point to the target * point without intervening obstructions. * * Does not take into account resistance less than opaque or distance cost. * * Uses RadiusStrategy.CIRCLE, or whatever RadiusStrategy was set with setRadiusStrategy . * * @param resistanceMap 0.0 is fully transparent, 1.0 is fully opaque, as always this uses x,y indexing. * @param startx starting x position on the grid * @param starty starting y position on the grid * @param targetx ending x position on the grid * @param targety ending y position on the grid * @return true if a line can be drawn without being obstructed, false otherwise */ public boolean isReachable(double[][] resistanceMap, int startx, int starty, int targetx, int targety) { return isReachable(resistanceMap, startx, starty, targetx, targety, radiusStrategy); } /** * Returns true if a line can be drawn from the start point to the target * point without intervening obstructions. * * @param resistanceMap 0.0 is fully transparent, 1.0 is fully opaque, as always this uses x,y indexing. * @param startx starting x position on the grid * @param starty starting y position on the grid * @param targetx ending x position on the grid * @param targety ending y position on the grid * @param radiusStrategy the strategy to use in computing unit distance * @return true if a line can be drawn without being obstructed, false otherwise */ public boolean isReachable(double[][] resistanceMap, int startx, int starty, int targetx, int targety, Radius radiusStrategy) { if(resistanceMap.length < 1) return false; this.resistanceMap = resistanceMap; this.startx = startx; this.starty = starty; this.targetx = targetx; this.targety = targety; switch (type) { case BRESENHAM: return bresenhamReachable(radiusStrategy); case ELIAS: return eliasReachable(radiusStrategy); case RAY: return rayReachable(radiusStrategy); case ORTHO: return orthoReachable(radiusStrategy); case DDA: return ddaReachable(radiusStrategy); case THICK: return thickReachable(radiusStrategy); } return false; } /** * Returns true if a line can be drawn from the start point to the target * point without intervening obstructions. * * @param walls '#' is fully opaque, anything else is fully transparent, as always this uses x,y indexing. * @param startx starting x position on the grid * @param starty starting y position on the grid * @param targetx ending x position on the grid * @param targety ending y position on the grid * @param radiusStrategy the strategy to use in computing unit distance * @return true if a line can be drawn without being obstructed, false otherwise */ public boolean isReachable(char[][] walls, int startx, int starty, int targetx, int targety, Radius radiusStrategy) { if(walls.length < 1) return false; double[][] resMap = new double[walls.length][walls[0].length]; for(int x = 0; x < walls.length; x++) { for(int y = 0; y < walls[0].length; y++) { resMap[x][y] = (walls[x][y] == '#') ? 1.0 : 0.0; } } return isReachable(resMap, startx, starty, targetx, targety, radiusStrategy); } /** * Returns true if a line can be drawn from the any of the points within spread cells of the start point, * to any of the corresponding points at the same direction and distance from the target point, without * intervening obstructions. Primarily useful to paint a broad line that can be retrieved with getLastPath. * * @param walls '#' is fully opaque, anything else is fully transparent, as always this uses x,y indexing. * @param startx starting x position on the grid * @param starty starting y position on the grid * @param targetx ending x position on the grid * @param targety ending y position on the grid * @param radiusStrategy the strategy to use in computing unit distance * @param spread the number of cells outward, measured by radiusStrategy, to place extra start and target points * @return true if a line can be drawn without being obstructed, false otherwise */ public boolean spreadReachable(char[][] walls, int startx, int starty, int targetx, int targety, Radius radiusStrategy, int spread) { if(walls.length < 1) return false; resistanceMap = new double[walls.length][walls[0].length]; for(int x = 0; x < walls.length; x++) { for(int y = 0; y < walls[0].length; y++) { resistanceMap[x][y] = (walls[x][y] == '#') ? 1.0 : 0.0; } } this.startx = startx; this.starty = starty; this.targetx = targetx; this.targety = targety; return brushReachable(radiusStrategy, spread); } /** * Returns true if a line can be drawn from the any of the points within spread cells of the start point, * to any of the corresponding points at the same direction and distance from the target point, without * intervening obstructions. Primarily useful to paint a broad line that can be retrieved with getLastPath. * * @param resistanceMap 0.0 is fully transparent, 1.0 is fully opaque, as always this uses x,y indexing. * @param startx starting x position on the grid * @param starty starting y position on the grid * @param targetx ending x position on the grid * @param targety ending y position on the grid * @param radiusStrategy the strategy to use in computing unit distance * @param spread the number of cells outward, measured by radiusStrategy, to place extra start and target points * @return true if a line can be drawn without being obstructed, false otherwise */ public boolean spreadReachable(double[][] resistanceMap, int startx, int starty, int targetx, int targety, Radius radiusStrategy, int spread) { if(resistanceMap.length < 1) return false; this.resistanceMap = resistanceMap; this.startx = startx; this.starty = starty; this.targetx = targetx; this.targety = targety; return brushReachable(radiusStrategy, spread); } /** * Returns the path of the last LOS calculation, with the starting point as * the head of the queue. * * @return the last path found during LOS calculation, as a ArrayDeque of Coord with the starting point at the head */ public ArrayDeque getLastPath() { return lastPath; } /* private boolean bresenhamReachable(Radius radiusStrategy) { Queue path = Bresenham.line2D(startx, starty, targetx, targety); lastPath = new ArrayDeque<>(); lastPath.add(Coord.get(startx, starty)); double decay = 1 / radiusStrategy.radius(startx, starty, targetx, targety); double currentForce = 1; for (Coord p : path) { lastPath.offer(p); if (p.x == targetx && p.y == targety) { return true;//reached the end } if (p.x != startx || p.y != starty) {//don't discount the start location even if on resistant cell currentForce -= resistanceMap[p.x][p.y]; } double r = radiusStrategy.radius(startx, starty, p.x, p.y); if (currentForce - (r * decay) <= 0) { return false;//too much resistance } } return false;//never got to the target point } */ private boolean bresenhamReachable(Radius radiusStrategy) { Coord[] path = Bresenham.line2D_(startx, starty, targetx, targety); lastPath = new ArrayDeque<>(); double rad = radiusStrategy.radius(startx, starty, targetx, targety); if(rad == 0.0) { lastPath.add(Coord.get(startx, starty)); return true; // already at the point; we can see our own feet just fine! } double decay = 1 / rad; double currentForce = 1; Coord p; for (int i = 0; i < path.length; i++) { p = path[i]; lastPath.offer(p); if (p.x == targetx && p.y == targety) { return true;//reached the end } if (p.x != startx || p.y != starty) {//don't discount the start location even if on resistant cell currentForce -= resistanceMap[p.x][p.y]; } double r = radiusStrategy.radius(startx, starty, p.x, p.y); if (currentForce - (r * decay) <= 0) { return false;//too much resistance } } return false;//never got to the target point } private boolean orthoReachable(Radius radiusStrategy) { Coord[] path = OrthoLine.line_(startx, starty, targetx, targety); lastPath = new ArrayDeque<>(); double rad = radiusStrategy.radius(startx, starty, targetx, targety); if(rad == 0.0) { lastPath.add(Coord.get(startx, starty)); return true; // already at the point; we can see our own feet just fine! } double decay = 1 / rad; double currentForce = 1; Coord p; for (int i = 0; i < path.length; i++) { p = path[i]; lastPath.offer(p); if (p.x == targetx && p.y == targety) { return true;//reached the end } if (p.x != startx || p.y != starty) {//don't discount the start location even if on resistant cell currentForce -= resistanceMap[p.x][p.y]; } double r = radiusStrategy.radius(startx, starty, p.x, p.y); if (currentForce - (r * decay) <= 0) { return false;//too much resistance } } return false;//never got to the target point } private boolean ddaReachable(Radius radiusStrategy) { Coord[] path = DDALine.line_(startx, starty, targetx, targety); lastPath = new ArrayDeque<>(); double rad = radiusStrategy.radius(startx, starty, targetx, targety); if(rad == 0.0) { lastPath.add(Coord.get(startx, starty)); return true; // already at the point; we can see our own feet just fine! } double decay = 1 / rad; double currentForce = 1; Coord p; for (int i = 0; i < path.length; i++) { p = path[i]; if (p.x == targetx && p.y == targety) { lastPath.offer(p); return true;//reached the end } if (p.x != startx || p.y != starty) {//don't discount the start location even if on resistant cell currentForce -= resistanceMap[p.x][p.y]; } double r = radiusStrategy.radius(startx, starty, p.x, p.y); if (currentForce - (r * decay) <= 0) { return false;//too much resistance } lastPath.offer(p); } return false;//never got to the target point } private boolean thickReachable(Radius radiusStrategy) { lastPath = new ArrayDeque<>(); double dist = radiusStrategy.radius(startx, starty, targetx, targety); double decay = 1.0 / dist; // note: decay can be positive infinity if dist is 0; this is actually OK OrderedSet visited = new OrderedSet<>((int) dist + 3); List> paths = new ArrayList<>(4); /* // actual corners paths.add(DDALine.line(startx, starty, targetx, targety, 0, 0)); paths.add(DDALine.line(startx, starty, targetx, targety, 0, 0xffff)); paths.add(DDALine.line(startx, starty, targetx, targety, 0xffff, 0)); paths.add(DDALine.line(startx, starty, targetx, targety, 0xffff, 0xffff)); */ // halfway between the center and a corner paths.add(DDALine.line(startx, starty, targetx, targety, 0x3fff, 0x3fff)); paths.add(DDALine.line(startx, starty, targetx, targety, 0x3fff, 0xbfff)); paths.add(DDALine.line(startx, starty, targetx, targety, 0xbfff, 0x3fff)); paths.add(DDALine.line(startx, starty, targetx, targety, 0xbfff, 0xbfff)); int length = Math.max(paths.get(0).size(), Math.max(paths.get(1).size(), Math.max(paths.get(2).size(), paths.get(3).size()))); double[] forces = new double[]{1,1,1,1}; boolean[] go = new boolean[]{true, true, true, true}; Coord p; for (int d = 0; d < length; d++) { for (int pc = 0; pc < 4; pc++) { List path = paths.get(pc); if(d < path.size() && go[pc]) p = path.get(d); else continue; if (p.x == targetx && p.y == targety) { visited.add(p); lastPath.addAll(visited); return true;//reached the end } if (p.x != startx || p.y != starty) {//don't discount the start location even if on resistant cell forces[pc] -= resistanceMap[p.x][p.y]; } double r = radiusStrategy.radius(startx, starty, p.x, p.y); if (forces[pc] - (r * decay) <= 0) { go[pc] = false; continue;//too much resistance } visited.add(p); } } lastPath.addAll(visited); return false;//never got to the target point } private boolean brushReachable(Radius radiusStrategy, int spread) { lastPath.clear(); double dist = radiusStrategy.radius(startx, starty, targetx, targety) + spread * 2; OrderedSet visited = new OrderedSet<>((int) (dist + 3) * spread); // List> paths = new ArrayList<>((int) (radiusStrategy.volume2D(spread) * 1.25)); // int length = 0; // List currentPath; int sx = startx, sy = starty, tx = targetx, ty = targety; for (int i = -spread; i <= spread; i++) { startx = sx + i; targetx = tx + i; for (int j = -spread; j <= spread; j++) { if(radiusStrategy.inRange(sx, sy, sx + i, sy + j, 0, spread) && sx + i >= 0 && sy + j >= 0 && sx + i < resistanceMap.length && sy + j < resistanceMap[0].length && tx + i >= 0 && ty + j >= 0 && tx + i < resistanceMap.length && ty + j < resistanceMap[0].length) { // for (int q = 0x3fff; q < 0xffff; q += 0x8000) { // for (int r = 0x3fff; r < 0xffff; r += 0x8000) { starty = sy + j; targety = ty + j; if(ddaReachable(radiusStrategy)) visited.addAll(lastPath); // currentPath = DDALine.line(startx+i, starty+j, targetx+i, targety+j, q, r); // paths.add(currentPath); // length = Math.max(length, currentPath.size()); // } // } } } } lastPath.clear(); lastPath.addAll(visited); return !lastPath.isEmpty(); // double force; //// boolean[] go = new boolean[paths.size()]; //// Arrays.fill(go, true); // Coord p; //// boolean found = false; // for (int pc = 0; pc < paths.size(); pc++) { // List path = paths.get(pc); // force = 1.0; // for (int d = 0; d < path.size(); d++) { // p = path.get(d); // //else continue; //// if (p.x == targetx && p.y == targety) { //// found = true; //// } // if (p.x != startx || p.y != starty) {//don't discount the start location even if on resistant cell // force -= resistanceMap[p.x][p.y]; // } // //double r = radiusStrategy.radius(startx, starty, p.x, p.y); // if (force <= 0) { // //go[pc] = false; // break;//too much resistance // } // visited.add(p); // } // } // lastPath.addAll(visited); // return true;//visited.contains(Coord.get(targetx, targety)); } private boolean rayReachable(Radius radiusStrategy) { lastPath = new ArrayDeque<>();//save path for later retrieval if (startx == targetx && starty == targety) {//already there! lastPath.add(Coord.get(startx, starty)); return true; } int width = resistanceMap.length; int height = resistanceMap[0].length; Coord end = Coord.get(targetx, targety); //find out which direction to step, on each axis int stepX = targetx == startx ? 0 : (targetx - startx >> 31 | 1), // signum with less converting to/from float stepY = targety == starty ? 0 : (targety - starty >> 31 | 1); int deltaY = Math.abs(targetx - startx), deltaX = Math.abs(targety - starty); int testX = startx, testY = starty; int maxX = deltaX, maxY = deltaY; while (testX >= 0 && testX < width && testY >= 0 && testY < height && (testX != targetx || testY != targety)) { lastPath.add(Coord.get(testX, testY)); if (maxY - maxX > deltaX) { maxX += deltaX; testX += stepX; if (resistanceMap[testX][testY] >= 1f) { end = Coord.get(testX, testY); break; } } else if (maxX - maxY > deltaY) { maxY += deltaY; testY += stepY; if (resistanceMap[testX][testY] >= 1f) { end = Coord.get(testX, testY); break; } } else {//directly on diagonal, move both full step maxY += deltaY; testY += stepY; maxX += deltaX; testX += stepX; if (resistanceMap[testX][testY] >= 1f) { end = Coord.get(testX, testY); break; } } if (radiusStrategy.radius(testX, testY, startx, starty) > radiusStrategy.radius(startx, starty, end.x, end.y)) {//went too far break; } } if (end.x >= 0 && end.x < width && end.y >= 0 && end.y < height) { lastPath.add(Coord.get(end.x, end.y)); } return end.x == targetx && end.y == targety; } private boolean eliasReachable(Radius radiusStrategy) { if(elias == null) { elias = new Elias(); los1 = new LOS(BRESENHAM); los2 = new LOS(BRESENHAM); } final ArrayList ePath = elias.line(startx, starty, targetx, targety); // // very similar to RNG.shuffleInPlace(); this may be handy for getting an early shortcut return // final int n = ePath.size(); // long state = 0x58476D1CE4E5B9BFL; // for (int i = n; i > 1; i--) { // // inlined LightRNG.determineBounded(); can replace *= with usually-faster += // Collections.swap(ePath, (int)((i * (((state = ((state = ((state += 0x9E3779B97F4A7C15L) ^ state >>> 30) * 0xBF58476D1CE4E5B9L) ^ state >>> 27) * 0x94D049BB133111EBL) ^ state >>> 31) & 0x7FFFFFFFL)) >> 31), i - 1); // } for(Coord p : ePath) { //if a non-solid midpoint on the path can see both the start and end, consider the two ends to be able to see each other if (resistanceMap[p.x][p.y] < 1 && radiusStrategy.radius(startx, starty, p.x, p.y) <= radiusStrategy.radius(startx, starty, targetx, targety) && los1.isReachable(resistanceMap, p.x, p.y, targetx, targety, radiusStrategy) && los2.isReachable(resistanceMap, startx, starty, p.x, p.y, radiusStrategy)) { //record actual sight path used lastPath.clear(); lastPath.addAll(los2.lastPath); lastPath.remove(p); // should be the only overlapping point lastPath.addAll(los1.lastPath); return true; } } return false;//never got to the target point } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy