|
- """
- 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
|