4 // Adam Milazzo's "my algorithm" for FOV. Adapted from the c# code at http://www.adammil.net/blog/v125_Roguelike_Vision_Algorithms.html
6 void do_fov(ref Map map
, uint x
, uint y
, int rangeLimit
) {
7 map
.for_all((ref Tile x
) => x
.visible
= false);
9 map
[y
, x
].visible
= true;
10 foreach (octant
; 0 .. 8) {
11 compute(map
, octant
, Point(x
, y
), rangeLimit
, 1, Slope(1, 1), Slope(0, 1));
15 // represents the slope Y/X as a rational number
16 private struct Slope
{
19 bool greater(int y
, int x
) { return this.y
*x
> this.x
*y
; } // this > y/x
20 bool greater_or_equal(int y
, int x
) { return this.y
*x
>= this.x
*y
; } // this >= y/x
21 bool less(int y
, int x
) { return this.y
*x
< this.x
*y
; } // this < y/x
22 bool less_or_equal(int y
, int x
) { return this.y
*x
<= this.x
*y
; } // this <= y/x
25 private struct Point
{
30 private void compute(ref Map map
, int octant
, Point origin
, int rangeLimit
, int x
, Slope top
, Slope bottom
) {
31 // NOTE: the code duplication between blocks_light and set_visible is for performance. don't refactor the octant
32 // translation out unless you don't mind an 18% drop in speed
33 bool blocks_light(int x
, int y
, int octant
, Point origin
) {
34 int nx
= origin
.x
, ny
= origin
.y
;
36 case 0: nx
+= x
; ny
-= y
; break;
37 case 1: nx
+= y
; ny
-= x
; break;
38 case 2: nx
-= y
; ny
-= x
; break;
39 case 3: nx
-= x
; ny
-= y
; break;
40 case 4: nx
-= x
; ny
+= y
; break;
41 case 5: nx
-= y
; ny
+= x
; break;
42 case 6: nx
+= y
; ny
+= x
; break;
43 case 7: nx
+= x
; ny
+= y
; break;
46 return map
[ny
, nx
].blocks_light
;
49 void set_visible(int x
, int y
, int octant
, Point origin
) {
50 int nx
= origin
.x
, ny
= origin
.y
;
52 case 0: nx
+= x
; ny
-= y
; break;
53 case 1: nx
+= y
; ny
-= x
; break;
54 case 2: nx
-= y
; ny
-= x
; break;
55 case 3: nx
-= x
; ny
-= y
; break;
56 case 4: nx
-= x
; ny
+= y
; break;
57 case 5: nx
-= y
; ny
+= x
; break;
58 case 6: nx
+= y
; ny
+= x
; break;
59 case 7: nx
+= x
; ny
+= y
; break;
62 map
[ny
, nx
].visible
= true;
70 * throughout this function there are references to various parts of tiles. a tile's coordinates refer to its
71 * center, and the following diagram shows the parts of the tile and the vectors from the origin that pass through
72 * those parts. given a part of a tile with vector u, a vector v passes above it if v > u and below it if v < u
74 * a------b a top left: (y*2+1) / (x*2-1) i inner top left: (y*4+1) / (x*4-1)
75 * | /\ | b top right: (y*2+1) / (x*2+1) j inner top right: (y*4+1) / (x*4+1)
76 * |i/__\j| c bottom left: (y*2-1) / (x*2-1) k inner bottom left: (y*4-1) / (x*4-1)
77 *e|/| |\|f d bottom right: (y*2-1) / (x*2+1) m inner bottom right: (y*4-1) / (x*4+1)
78 * |\|__|/| e middle left: (y*2) / (x*2-1)
79 * |k\ /m| f middle right: (y*2) / (x*2+1) a-d are the corners of the tile
80 * | \/ | g top center: (y*2+1) / (x*2) e-h are the corners of the inner (wall) diamond
81 * c------d h bottom center: (y*2-1) / (x*2) i-m are the corners of the inner square (1/2 tile width)
84 for(; x
<= rangeLimit
; x
++) /* (x <= rangeLimit) == (rangeLimit < 0 || x <= rangeLimit) */ {
85 // compute the Y coordinates of the top and bottom of the sector. we maintain that top > bottom
88 // if top == ?/1 then it must be 1/1 because 0/1 < top <= 1/1. this is special-cased because top
89 // starts at 1/1 and remains 1/1 as long as it doesn't hit anything, so it's a common case
95 * get the tile that the top vector enters from the left. since our coordinates refer to the center of the
96 * tile, this is (x-0.5)*top+0.5, which can be computed as (x-0.5)*top+0.5 = (2(x+0.5)*top+1)/2 =
97 * ((2x+1)*top+1)/2. since top == a/b, this is ((2x+1)*a+b)/2b. if it enters a tile at one of the left
98 * corners, it will round up, so it'll enter from the bottom-left and never the top-left
100 // the Y coordinate of the tile entered from the left
101 topY
= ((x
*2-1) * top
.y
+ top
.x
) / (top
.x
*2);
102 /* now it's possible that the vector passes from the left side of the tile up into the tile above before
103 * exiting from the right side of this column. so we may need to increment topY
106 // if the tile blocks light (i.e. is a wall)...
107 if (blocks_light(x
, topY
, octant
, origin
)) {
108 /* if the tile entered from the left blocks light, whether it
109 * passes into the tile above depends on the shape of the wall tile
110 * as well as the angle of the vector. if the tile has does not
111 * have a beveled top-left corner, then it is blocked. the corner is
112 * beveled if the tiles above and to the left are not walls. we can
113 * ignore the tile to the left because if it was a wall tile, the
114 * top vector must have entered this tile from the bottom-left
115 * corner, in which case it can't possibly enter the tile above.
117 * otherwise, with a beveled top-left corner, the slope of the
118 * vector must be greater than or equal to the slope of the vector
119 * to the top center of the tile (x*2, topY*2+1) in order for it to
120 * miss the wall and pass into the tile above
122 if (top
.greater_or_equal(topY
*2+1, x
*2) && !blocks_light(x
, topY
+1, octant
, origin
)) {
126 // the tile doesn't block light
128 /* since this tile doesn't block light, there's nothing to stop it
129 * from passing into the tile above, and it does so if the vector is greater than the
130 * vector for the bottom-right corner of the tile above. however,
131 * there is one additional consideration. later code in this method
132 * assumes that if a tile blocks light then it must be visible, so
133 * if the tile above blocks light we have to make sure the light
134 * actually impacts the wall shape. now there are three cases:
136 * 1) the tile above is clear, in which case the vector must be
137 * above the bottom-right corner of the tile above,
139 * 2) the tile above blocks light and does not have a beveled
140 * bottom-right corner, in which case the vector must be above
141 * the bottom-right corner, and
143 * 3) the tile above blocks light and does have a beveled bottom-
144 * right corner, in which case the vector must be above the bottom
145 * center of the tile above (i.e. the corner of the beveled edge).
148 * now it's possible to merge 1 and 2 into a single check, and we
149 * get the following: if the tile above and to the right is a wall,
150 * then the vector must be above the bottom-right corner. otherwise,
151 * the vector must be above the bottom center. this works because if
152 * the tile above and to the right is a wall, then there are two
155 * 1) the tile above is also a wall, in which case we must check
156 * against the bottom-right corner, or
158 * 2) the tile above is not a wall, in which case the vector passes
159 * into it if it's above the bottom-right corner.
162 * so either way we use the bottom-right corner in that case. now,
163 * if the tile above and to the right is not a wall, then we again
166 * 1) the tile above is a wall with a beveled edge, in which case we
167 * must check against the bottom center, or
169 * 2) the tile above is not a wall, in which case it will only be
170 * visible if light passes through the inner square, and the
171 * inner square is guaranteed to be no larger than a wall
172 * diamond, so if it wouldn't pass through a wall diamond then it
173 * can't be visible, so there's no point in incrementing topY
174 * even if light passes through the corner of the tile above. so
175 * we might as well use the bottom center for both cases.
177 int ax
= x
*2; // center
179 // use bottom-right if the tile above and right is a wall
180 if (blocks_light(x
+1, topY
+1, octant
, origin
)) {
184 if (top
.greater(topY
*2+1, ax
)) {
191 // if bottom == 0/?, then it's hitting the tile at Y=0 dead center. this is special-cased because
192 // bottom.y starts at zero and remains zero as long as it doesn't hit anything, so it's common
198 // the tile that the bottom vector enters from the left
199 bottomY
= ((x
*2-1) * bottom
.y
+ bottom
.x
) / (bottom
.x
*2);
202 * code below assumes that if a tile is a wall then it's visible, so if the tile contains a wall we have to
203 * ensure that the bottom vector actually hits the wall shape. it misses the wall shape if the top-left corner
204 * is beveled and bottom >= (bottomY*2+1)/(x*2). finally, the top-left corner is beveled if the tiles to the
205 * left and above are clear. we can assume the tile to the left is clear because otherwise the bottom vector
206 * would be greater, so we only have to check above
208 if (bottom
.greater_or_equal(bottomY
*2+1, x
*2) &&
209 blocks_light(x
, bottomY
, octant
, origin
) &&
210 !blocks_light(x
, bottomY
+1, octant
, origin
)) {
215 // go through the tiles in the column now that we know which ones could possibly be visible
216 int wasOpaque
= -1; // 0:false, 1:true, -1:not applicable
218 // use a signed comparison because y can wrap around when decremented
219 for (int y
= topY
; y
>= bottomY
; y
--) {
220 // skip the tile if it's out of visual range
221 if (rangeLimit
< 0 ||
get_distance(x
, y
) <= rangeLimit
) {
222 bool isOpaque
= blocks_light(x
, y
, octant
, origin
);
224 * every tile where topY > y > bottomY is guaranteed to be visible.
225 * also, the code that initializes topY and bottomY guarantees that
226 * if the tile is opaque then it's visible. so we only have to do
227 * extra work for the case where the tile is clear and y == topY or
228 * y == bottomY. if y == topY then we have to make sure that the top
229 * vector is above the bottom-right corner of the inner square.
230 * if y == bottomY then we have to make sure that the bottom vector
231 * is below the top-left corner of the inner square
233 bool isVisible
= isOpaque ||
((y
!= topY || top
.greater(y
*4-1, x
*4+1)) && (y
!= bottomY || bottom
.less(y
*4+1, x
*4-1)));
235 * NOTE: if you want the algorithm to be either fully or mostly
236 * symmetrical, replace the line above with the following line (and
237 * uncomment the Slope.less_or_equal method). the line ensures that a
238 * clear tile is visible only if there's an unobstructed line to its
239 * center. if you want it to be fully symmetrical, also remove the
240 * "isOpaque ||" part and see NOTE comments further down
242 * bool isVisible = isOpaque || ((y != topY || top.greaterOrEqual(y, x)) && (y != bottomY || bottom.less_or_equal(y, x)));
245 set_visible(x
, y
, octant
, origin
);
248 // if we found a transition from clear to opaque or vice versa, adjust the top and bottom vectors
249 // but don't bother adjusting them if this is the last column anyway
250 if (x
!= rangeLimit
) {
252 // if we found a transition from clear to opaque, this sector is done in this column,
253 // so adjust the bottom vector upward and continue processing it in the next column
254 if (wasOpaque
== 0) {
256 * if the opaque tile has a beveled top-left
257 * corner, move the bottom vector up to the
258 * top center. otherwise, move it up to the
259 * top left. the corner is beveled if the
260 * tiles above and to the left are clear. we
261 * can assume the tile to the left is clear
262 * because otherwise the vector would be
263 * higher, so we only have to check the tile
266 int nx
= x
*2, ny
= y
*2+1; // top center by default
267 // NOTE: if you're using full symmetry and
268 // want more expansive walls (recommended),
269 // comment out the next line
271 // top left if the corner is not beveled
272 if (blocks_light(x
, y
+1, octant
, origin
))
275 // we have to maintain the invariant that top > bottom, so the new sector
276 // created by adjusting the bottom is only valid if that's the case
277 if (top
.greater(ny
, nx
)) {
278 // if we're at the bottom of the column,
279 // then just adjust the current sector rather than recursing
280 // since there's no chance that this sector
281 // can be split in two by a later transition back to clear
283 bottom
= Slope(ny
, nx
); break;
285 // don't recurse unless necessary
287 compute(map
, octant
, origin
, rangeLimit
, x
+1, top
, Slope(ny
, nx
));
290 // the new bottom is greater than or equal
291 // to the top, so the new sector is empty
292 // and we'll ignore it. if we're at the
293 // bottom of the column, we'd normally
294 // adjust the current sector rather than
296 // recursing, so that invalidates the current sector and we're done
303 // if we found a transition from opaque to clear, adjust the top vector downwards
305 // if the opaque tile has a beveled bottom-
306 // right corner, move the top vector down to
307 // the bottom center. otherwise, move it
308 // down to the bottom right. the corner is
309 // beveled if the tiles below and to the
310 // right are clear. we know the tile below
311 // is clear because that's the current tile,
312 // so just check to the right
314 // the bottom of the opaque tile (oy*2-1) equals the top of this tile (y*2+1)
315 int nx
= x
*2, ny
= y
*2+1;
317 // NOTE: if you're using full symmetry and
318 // want more expansive walls (recommended),
319 // comment out the next line
321 // check the right of the opaque tile (y+1), not this one
322 if (blocks_light(x
+1, y
+1, octant
, origin
)) {
326 // we have to maintain the invariant that
327 // top > bottom. if not, the sector is empty
329 if (bottom
.greater_or_equal(ny
, nx
)) {
341 // if the column didn't end in a clear tile, then there's no reason to continue processing the current sector
342 // because that means either 1) wasOpaque == -1, implying that the sector is empty or at its range limit, or 2)
343 // wasOpaque == 1, implying that we found a transition from clear to opaque and we recursed and we never found
344 // a transition back to clear, so there's nothing else for us to do that the recursive method hasn't already. (if
345 // we didn't recurse (because y == bottomY), it would have executed a break, leaving wasOpaque equal to 0.)
352 private int get_distance(int x
, int y
) {
353 return x
* x
+ y
* y
;