This is an implementation of the Precise Permissive Field of View algorithm in Python.
The algorithm is contained in the module fov. The user calls the function fieldOfView(), giving it an (x, y) coordinate, the width and height of the map, the view radius, and two functions which take (x, y) coordinates and either visits the coordinate for the user or test whether the coordinate blocks sight.
"""
Author: Aaron MacDonald
Date: June 14, 2007
Description: An implementation of the precise permissive field
of view algorithm for use in tile-based games.
Based on the algorithm presented at
http://roguebasin.roguelikedevelopment.org/
index.php?title=
Precise_Permissive_Field_of_View.
You are free to use or modify this code as long as this notice is
included.
This code is released without warranty.
"""
import copy
def fieldOfView(startX, startY, mapWidth, mapHeight, radius, \
funcVisitTile, funcTileBlocked):
"""
Determines which coordinates on a 2D grid are visible from a
particular coordinate.
startX, startY: The (x, y) coordinate on the grid that
is the centre of view.
mapWidth, mapHeight: The maximum extents of the grid. The
minimum extents are assumed to be both
zero.
radius: How far the field of view may extend
in either direction along the x and y
axis.
funcVisitTile: User function that takes two integers
representing an (x, y) coordinate. Is
used to "visit" visible coordinates.
funcTileBlocked: User function that takes two integers
representing an (x, y) coordinate.
Returns True if the coordinate blocks
sight to coordinates "behind" it.
"""
visited = set() # Keep track of what tiles have been visited so
# that no tile will be visited twice.
# Will always see the centre.
funcVisitTile(startX, startY)
visited.add((startX, startY))
# Ge the dimensions of the actual field of view, making
# sure not to go off the map or beyond the radius.
if startX < radius:
minExtentX = startX
else:
minExtentX = radius
if mapWidth - startX - 1 < radius:
maxExtentX = mapWidth - startX - 1
else:
maxExtentX = radius
if startY < radius:
minExtentY = startY
else:
minExtentY = radius
if mapHeight - startY - 1 < radius:
maxExtentY = mapHeight - startY - 1
else:
maxExtentY = radius
# Northeast quadrant
__checkQuadrant(visited, startX, startY, 1, 1, \
maxExtentX, maxExtentY, \
funcVisitTile, funcTileBlocked)
# Southeast quadrant
__checkQuadrant(visited, startX, startY, 1, -1, \
maxExtentX, minExtentY, \
funcVisitTile, funcTileBlocked)
# Southwest quadrant
__checkQuadrant(visited, startX, startY, -1, -1, \
minExtentX, minExtentY, \
funcVisitTile, funcTileBlocked)
# Northwest quadrant
__checkQuadrant(visited, startX, startY, -1, 1, \
minExtentX, maxExtentY, \
funcVisitTile, funcTileBlocked)
#-------------------------------------------------------------
class __Line(object):
def __init__(self, xi, yi, xf, yf):
self.xi = xi
self.yi = yi
self.xf = xf
self.yf = yf
dx = property(fget = lambda self: self.xf - self.xi)
dy = property(fget = lambda self: self.yf - self.yi)
def pBelow(self, x, y):
return self.relativeSlope(x, y) > 0
def pBelowOrCollinear(self, x, y):
return self.relativeSlope(x, y) >= 0
def pAbove(self, x, y):
return self.relativeSlope(x, y) < 0
def pAboveOrCollinear(self, x, y):
return self.relativeSlope(x, y) <= 0
def pCollinear(self, x, y):
return self.relativeSlope(x, y) == 0
def lineCollinear(self, line):
return self.pCollinear(line.xi, line.yi) \
and self.pCollinear(line.xf, line.yf)
def relativeSlope(self, x, y):
return (self.dy * (self.xf - x)) \
- (self.dx * (self.yf - y))
class __ViewBump:
def __init__(self, x, y, parent):
self.x = x
self.y = y
self.parent = parent
class __View:
def __init__(self, shallowLine, steepLine):
self.shallowLine = shallowLine
self.steepLine = steepLine
self.shallowBump = None
self.steepBump = None
def __checkQuadrant(visited, startX, startY, dx, dy, \
extentX, extentY, funcVisitTile, funcTileBlocked):
activeViews = []
shallowLine = __Line(0, 1, extentX, 0)
steepLine = __Line(1, 0, 0, extentY)
activeViews.append( __View(shallowLine, steepLine) )
viewIndex = 0
# Visit the tiles diagonally and going outwards
#
# .
# .
# . .
# 9 .
# 5 8 .
# 2 4 7
# @ 1 3 6 . . .
maxI = extentX + extentY
i = 1
while i != maxI + 1 and len(activeViews) > 0:
if 0 > i - extentX:
startJ = 0
else:
startJ = i - extentX
if i < extentY:
maxJ = i
else:
maxJ = extentY
j = startJ
while j != maxJ + 1 and viewIndex < len(activeViews):
x = i - j
y = j
__visitCoord(visited, startX, startY, x, y, dx, dy, \
viewIndex, activeViews, \
funcVisitTile, funcTileBlocked)
j += 1
i += 1
def __visitCoord(visited, startX, startY, x, y, dx, dy, viewIndex, \
activeViews, funcVisitTile, funcTileBlocked):
# The top left and bottom right corners of the current coordinate.
topLeft = (x, y + 1)
bottomRight = (x + 1, y)
while viewIndex < len(activeViews) \
and activeViews[viewIndex].steepLine.pBelowOrCollinear( \
bottomRight[0], bottomRight[1]):
# The current coordinate is above the current view and is
# ignored. The steeper fields may need it though.
viewIndex += 1
if viewIndex == len(activeViews) \
or activeViews[viewIndex].shallowLine.pAboveOrCollinear( \
topLeft[0], topLeft[1]):
# Either the current coordinate is above all of the fields
# or it is below all of the fields.
return
# It is now known that the current coordinate is between the steep
# and shallow lines of the current view.
isBlocked = False
# The real quadrant coordinates
realX = x * dx
realY = y * dy
if (startX + realX, startY + realY) not in visited:
visited.add((startX + realX, startY + realY))
funcVisitTile(startX + realX, startY + realY)
"""else:
# Debugging
print (startX + realX, startY + realY)"""
isBlocked = funcTileBlocked(startX + realX, startY + realY)
if not isBlocked:
# The current coordinate does not block sight and therefore
# has no effect on the view.
return
if activeViews[viewIndex].shallowLine.pAbove( \
bottomRight[0], bottomRight[1]) \
and activeViews[viewIndex].steepLine.pBelow( \
topLeft[0], topLeft[1]):
# The current coordinate is intersected by both lines in the
# current view. The view is completely blocked.
del activeViews[viewIndex]
elif activeViews[viewIndex].shallowLine.pAbove( \
bottomRight[0], bottomRight[1]):
# The current coordinate is intersected by the shallow line of
# the current view. The shallow line needs to be raised.
__addShallowBump(topLeft[0], topLeft[1], \
activeViews, viewIndex)
__checkView(activeViews, viewIndex)
elif activeViews[viewIndex].steepLine.pBelow( \
topLeft[0], topLeft[1]):
# The current coordinate is intersected by the steep line of
# the current view. The steep line needs to be lowered.
__addSteepBump(bottomRight[0], bottomRight[1], activeViews, \
viewIndex)
__checkView(activeViews, viewIndex)
else:
# The current coordinate is completely between the two lines
# of the current view. Split the current view into two views
# above and below the current coordinate.
shallowViewIndex = viewIndex
viewIndex += 1
steepViewIndex = viewIndex
activeViews.insert(shallowViewIndex, \
copy.deepcopy(activeViews[shallowViewIndex]))
__addSteepBump(bottomRight[0], bottomRight[1], \
activeViews, shallowViewIndex)
if not __checkView(activeViews, shallowViewIndex):
viewIndex -= 1
steepViewIndex -= 1
__addShallowBump(topLeft[0], topLeft[1], activeViews, \
steepViewIndex)
__checkView(activeViews, steepViewIndex)
def __addShallowBump(x, y, activeViews, viewIndex):
activeViews[viewIndex].shallowLine.xf = x
activeViews[viewIndex].shallowLine.yf = y
activeViews[viewIndex].shallowBump = __ViewBump(x, y, \
activeViews[viewIndex].shallowBump)
curBump = activeViews[viewIndex].steepBump
while curBump is not None:
if activeViews[viewIndex].shallowLine.pAbove( \
curBump.x, curBump.y):
activeViews[viewIndex].shallowLine.xi = curBump.x
activeViews[viewIndex].shallowLine.yi = curBump.y
curBump = curBump.parent
def __addSteepBump(x, y, activeViews, viewIndex):
activeViews[viewIndex].steepLine.xf = x
activeViews[viewIndex].steepLine.yf = y
activeViews[viewIndex].steepBump = __ViewBump(x, y, \
activeViews[viewIndex].steepBump)
curBump = activeViews[viewIndex].shallowBump
while curBump is not None:
if activeViews[viewIndex].steepLine.pBelow( \
curBump.x, curBump.y):
activeViews[viewIndex].steepLine.xi = curBump.x
activeViews[viewIndex].steepLine.yi = curBump.y
curBump = curBump.parent
def __checkView(activeViews, viewIndex):
"""
Removes the view in activeViews at index viewIndex if
- The two lines are coolinear
- The lines pass through either extremity
"""
shallowLine = activeViews[viewIndex].shallowLine
steepLine = activeViews[viewIndex].steepLine
if shallowLine.lineCollinear(steepLine) \
and ( shallowLine.pCollinear(0, 1) \
or shallowLine.pCollinear(1, 0) ):
del activeViews[viewIndex]
return False
else:
return True