2019-10-30 17:56:30 +03:00

258 lines
16 KiB
Go

package milazzo
/**
```cpp
sealed class MyVisibility : Visibility
{
/// <param name="blocksLight">A function that accepts the X and Y coordinates of a tile and determines whether the
/// given tile blocks the passage of light. The function must be able to accept coordinates that are out of bounds.
/// </param>
/// <param name="setVisible">A function that sets a tile to be visible, given its X and Y coordinates. The function
/// must ignore coordinates that are out of bounds.
/// </param>
/// <param name="getDistance">A function that takes the X and Y coordinate of a point where X >= 0,
/// Y >= 0, and X >= Y, and returns the distance from the point to the origin (0,0).
/// </param>
public MyVisibility(Func<int, int, bool> blocksLight, Action<int, int> setVisible, Func<int, int, int> getDistance)
{
_blocksLight = blocksLight;
GetDistance = getDistance;
_setVisible = setVisible;
}
public override void Compute(LevelPoint origin, int rangeLimit)
{
_setVisible(origin.X, origin.Y);
for(uint octant=0; octant<8; octant++) Compute(octant, origin, rangeLimit, 1, new Slope(1, 1), new Slope(0, 1));
}
struct Slope // represents the slope Y/X as a rational number
{
public Slope(uint y, uint x) { Y = y; X = x; }
public bool Greater(uint y, uint x) { return Y*x > X*y; } // this > y/x
public bool GreaterOrEqual(uint y, uint x) { return Y*x >= X*y; } // this >= y/x
public bool Less(uint y, uint x) { return Y*x < X*y; } // this < y/x
//public bool LessOrEqual(uint y, uint x) { return Y*x <= X*y; } // this <= y/x
public readonly uint X, Y;
}
void Compute(uint octant, LevelPoint origin, int rangeLimit, uint x, Slope top, Slope bottom)
{
// throughout this function there are references to various parts of tiles. a tile's coordinates refer to its
// center, and the following diagram shows the parts of the tile and the vectors from the origin that pass through
// 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
// g center: y / x
// a------b a top left: (y*2+1) / (x*2-1) i inner top left: (y*4+1) / (x*4-1)
// | /\ | b top right: (y*2+1) / (x*2+1) j inner top right: (y*4+1) / (x*4+1)
// |i/__\j| c bottom left: (y*2-1) / (x*2-1) k inner bottom left: (y*4-1) / (x*4-1)
//e|/| |\|f d bottom right: (y*2-1) / (x*2+1) m inner bottom right: (y*4-1) / (x*4+1)
// |\|__|/| e middle left: (y*2) / (x*2-1)
// |k\ /m| f middle right: (y*2) / (x*2+1) a-d are the corners of the tile
// | \/ | g top center: (y*2+1) / (x*2) e-h are the corners of the inner (wall) diamond
// c------d h bottom center: (y*2-1) / (x*2) i-m are the corners of the inner square (1/2 tile width)
// h
for(; x <= (uint)rangeLimit; x++) // (x <= (uint)rangeLimit) == (rangeLimit < 0 || x <= rangeLimit)
{
// compute the Y coordinates of the top and bottom of the sector. we maintain that top > bottom
uint topY;
if(top.X == 1) // if top == ?/1 then it must be 1/1 because 0/1 < top <= 1/1. this is special-cased because top
{ // starts at 1/1 and remains 1/1 as long as it doesn't hit anything, so it's a common case
topY = x;
}
else // top < 1
{
// get the tile that the top vector enters from the left. since our coordinates refer to the center of the
// 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 =
// ((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
// corners, it will round up, so it'll enter from the bottom-left and never the top-left
topY = ((x*2-1) * top.Y + top.X) / (top.X*2); // the Y coordinate of the tile entered from the left
// now it's possible that the vector passes from the left side of the tile up into the tile above before
// exiting from the right side of this column. so we may need to increment topY
if(BlocksLight(x, topY, octant, origin)) // if the tile blocks light (i.e. is a wall)...
{
// if the tile entered from the left blocks light, whether it passes into the tile above depends on the shape
// of the wall tile as well as the angle of the vector. if the tile has does not have a beveled top-left
// corner, then it is blocked. the corner is beveled if the tiles above and to the left are not walls. we can
// ignore the tile to the left because if it was a wall tile, the top vector must have entered this tile from
// the bottom-left corner, in which case it can't possibly enter the tile above.
//
// otherwise, with a beveled top-left corner, the slope of the vector must be greater than or equal to the
// slope of the vector to the top center of the tile (x*2, topY*2+1) in order for it to miss the wall and
// pass into the tile above
if(top.GreaterOrEqual(topY*2+1, x*2) && !BlocksLight(x, topY+1, octant, origin)) topY++;
}
else // the tile doesn't block light
{
// since this tile doesn't block light, there's nothing to stop it from passing into the tile above, and it
// does so if the vector is greater than the vector for the bottom-right corner of the tile above. however,
// there is one additional consideration. later code in this method assumes that if a tile blocks light then
// it must be visible, so if the tile above blocks light we have to make sure the light actually impacts the
// wall shape. now there are three cases: 1) the tile above is clear, in which case the vector must be above
// the bottom-right corner of the tile above, 2) the tile above blocks light and does not have a beveled
// bottom-right corner, in which case the vector must be above the bottom-right corner, and 3) the tile above
// blocks light and does have a beveled bottom-right corner, in which case the vector must be above the
// bottom center of the tile above (i.e. the corner of the beveled edge).
//
// now it's possible to merge 1 and 2 into a single check, and we get the following: if the tile above and to
// the right is a wall, then the vector must be above the bottom-right corner. otherwise, the vector must be
// above the bottom center. this works because if the tile above and to the right is a wall, then there are
// two cases: 1) the tile above is also a wall, in which case we must check against the bottom-right corner,
// or 2) the tile above is not a wall, in which case the vector passes into it if it's above the bottom-right
// corner. so either way we use the bottom-right corner in that case. now, if the tile above and to the right
// is not a wall, then we again have two cases: 1) the tile above is a wall with a beveled edge, in which
// case we must check against the bottom center, or 2) the tile above is not a wall, in which case it will
// only be visible if light passes through the inner square, and the inner square is guaranteed to be no
// larger than a wall diamond, so if it wouldn't pass through a wall diamond then it can't be visible, so
// there's no point in incrementing topY even if light passes through the corner of the tile above. so we
// might as well use the bottom center for both cases.
uint ax = x*2; // center
if(BlocksLight(x+1, topY+1, octant, origin)) ax++; // use bottom-right if the tile above and right is a wall
if(top.Greater(topY*2+1, ax)) topY++;
}
}
uint bottomY;
if(bottom.Y == 0) // if bottom == 0/?, then it's hitting the tile at Y=0 dead center. this is special-cased because
{ // bottom.Y starts at zero and remains zero as long as it doesn't hit anything, so it's common
bottomY = 0;
}
else // bottom > 0
{
bottomY = ((x*2-1) * bottom.Y + bottom.X) / (bottom.X*2); // the tile that the bottom vector enters from the left
// code below assumes that if a tile is a wall then it's visible, so if the tile contains a wall we have to
// ensure that the bottom vector actually hits the wall shape. it misses the wall shape if the top-left corner
// is beveled and bottom >= (bottomY*2+1)/(x*2). finally, the top-left corner is beveled if the tiles to the
// left and above are clear. we can assume the tile to the left is clear because otherwise the bottom vector
// would be greater, so we only have to check above
if(bottom.GreaterOrEqual(bottomY*2+1, x*2) && BlocksLight(x, bottomY, octant, origin) &&
!BlocksLight(x, bottomY+1, octant, origin))
{
bottomY++;
}
}
// go through the tiles in the column now that we know which ones could possibly be visible
int wasOpaque = -1; // 0:false, 1:true, -1:not applicable
for(uint y = topY; (int)y >= (int)bottomY; y--) // use a signed comparison because y can wrap around when decremented
{
if(rangeLimit < 0 || GetDistance((int)x, (int)y) <= rangeLimit) // skip the tile if it's out of visual range
{
bool isOpaque = BlocksLight(x, y, octant, origin);
// every tile where topY > y > bottomY is guaranteed to be visible. also, the code that initializes topY and
// bottomY guarantees that if the tile is opaque then it's visible. so we only have to do extra work for the
// case where the tile is clear and y == topY or y == bottomY. if y == topY then we have to make sure that
// the top vector is above the bottom-right corner of the inner square. if y == bottomY then we have to make
// sure that the bottom vector is below the top-left corner of the inner square
bool isVisible =
isOpaque || ((y != topY || top.Greater(y*4-1, x*4+1)) && (y != bottomY || bottom.Less(y*4+1, x*4-1)));
// NOTE: if you want the algorithm to be either fully or mostly symmetrical, replace the line above with the
// following line (and uncomment the Slope.LessOrEqual method). the line ensures that a clear tile is visible
// only if there's an unobstructed line to its center. if you want it to be fully symmetrical, also remove
// the "isOpaque ||" part and see NOTE comments further down
// bool isVisible = isOpaque || ((y != topY || top.GreaterOrEqual(y, x)) && (y != bottomY || bottom.LessOrEqual(y, x)));
if(isVisible) SetVisible(x, y, octant, origin);
// if we found a transition from clear to opaque or vice versa, adjust the top and bottom vectors
if(x != rangeLimit) // but don't bother adjusting them if this is the last column anyway
{
if(isOpaque)
{
if(wasOpaque == 0) // if we found a transition from clear to opaque, this sector is done in this column,
{ // so adjust the bottom vector upward and continue processing it in the next column
// if the opaque tile has a beveled top-left corner, move the bottom vector up to the top center.
// otherwise, move it up to the top left. the corner is beveled if the tiles above and to the left are
// clear. we can assume the tile to the left is clear because otherwise the vector would be higher, so
// we only have to check the tile above
uint nx = x*2, ny = y*2+1; // top center by default
// NOTE: if you're using full symmetry and want more expansive walls (recommended), comment out the next line
if(BlocksLight(x, y+1, octant, origin)) nx--; // top left if the corner is not beveled
if(top.Greater(ny, nx)) // we have to maintain the invariant that top > bottom, so the new sector
{ // created by adjusting the bottom is only valid if that's the case
// if we're at the bottom of the column, then just adjust the current sector rather than recursing
// since there's no chance that this sector can be split in two by a later transition back to clear
if(y == bottomY) { bottom = new Slope(ny, nx); break; } // don't recurse unless necessary
else Compute(octant, origin, rangeLimit, x+1, top, new Slope(ny, nx));
}
else // the new bottom is greater than or equal to the top, so the new sector is empty and we'll ignore
{ // it. if we're at the bottom of the column, we'd normally adjust the current sector rather than
if(y == bottomY) return; // recursing, so that invalidates the current sector and we're done
}
}
wasOpaque = 1;
}
else
{
if(wasOpaque > 0) // if we found a transition from opaque to clear, adjust the top vector downwards
{
// if the opaque tile has a beveled bottom-right corner, move the top vector down to the bottom center.
// otherwise, move it down to the bottom right. the corner is beveled if the tiles below and to the right
// are clear. we know the tile below is clear because that's the current tile, so just check to the right
uint nx = x*2, ny = y*2+1; // the bottom of the opaque tile (oy*2-1) equals the top of this tile (y*2+1)
// NOTE: if you're using full symmetry and want more expansive walls (recommended), comment out the next line
if(BlocksLight(x+1, y+1, octant, origin)) nx++; // check the right of the opaque tile (y+1), not this one
// we have to maintain the invariant that top > bottom. if not, the sector is empty and we're done
if(bottom.GreaterOrEqual(ny, nx)) return;
top = new Slope(ny, nx);
}
wasOpaque = 0;
}
}
}
}
// if the column didn't end in a clear tile, then there's no reason to continue processing the current sector
// because that means either 1) wasOpaque == -1, implying that the sector is empty or at its range limit, or 2)
// wasOpaque == 1, implying that we found a transition from clear to opaque and we recursed and we never found
// a transition back to clear, so there's nothing else for us to do that the recursive method hasn't already. (if
// we didn't recurse (because y == bottomY), it would have executed a break, leaving wasOpaque equal to 0.)
if(wasOpaque != 0) break;
}
}
// NOTE: the code duplication between BlocksLight and SetVisible is for performance. don't refactor the octant
// translation out unless you don't mind an 18% drop in speed
bool BlocksLight(uint x, uint y, uint octant, LevelPoint origin)
{
uint nx = origin.X, ny = origin.Y;
switch(octant)
{
case 0: nx += x; ny -= y; break;
case 1: nx += y; ny -= x; break;
case 2: nx -= y; ny -= x; break;
case 3: nx -= x; ny -= y; break;
case 4: nx -= x; ny += y; break;
case 5: nx -= y; ny += x; break;
case 6: nx += y; ny += x; break;
case 7: nx += x; ny += y; break;
}
return _blocksLight((int)nx, (int)ny);
}
void SetVisible(uint x, uint y, uint octant, LevelPoint origin)
{
uint nx = origin.X, ny = origin.Y;
switch(octant)
{
case 0: nx += x; ny -= y; break;
case 1: nx += y; ny -= x; break;
case 2: nx -= y; ny -= x; break;
case 3: nx -= x; ny -= y; break;
case 4: nx -= x; ny += y; break;
case 5: nx -= y; ny += x; break;
case 6: nx += y; ny += x; break;
case 7: nx += x; ny += y; break;
}
_setVisible((int)nx, (int)ny);
}
readonly Func<int, int, bool> _blocksLight;
readonly Func<int, int, int> GetDistance;
readonly Action<int, int> _setVisible;
}
```
*/