You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

325 lines
10KB

  1. """
  2. Author: Aaron MacDonald
  3. Date: June 14, 2007
  4. Description: An implementation of the precise permissive field
  5. of view algorithm for use in tile-based games.
  6. Based on the algorithm presented at
  7. http://roguebasin.roguelikedevelopment.org/
  8. index.php?title=
  9. Precise_Permissive_Field_of_View.
  10. You are free to use or modify this code as long as this notice is
  11. included.
  12. This code is released without warranty.
  13. """
  14. import copy
  15. def fieldOfView(startX, startY, mapWidth, mapHeight, radius, \
  16. funcVisitTile, funcTileBlocked):
  17. """
  18. Determines which coordinates on a 2D grid are visible from a
  19. particular coordinate.
  20. startX, startY: The (x, y) coordinate on the grid that
  21. is the centre of view.
  22. mapWidth, mapHeight: The maximum extents of the grid. The
  23. minimum extents are assumed to be both
  24. zero.
  25. radius: How far the field of view may extend
  26. in either direction along the x and y
  27. axis.
  28. funcVisitTile: User function that takes two integers
  29. representing an (x, y) coordinate. Is
  30. used to "visit" visible coordinates.
  31. funcTileBlocked: User function that takes two integers
  32. representing an (x, y) coordinate.
  33. Returns True if the coordinate blocks
  34. sight to coordinates "behind" it.
  35. """
  36. visited = set() # Keep track of what tiles have been visited so
  37. # that no tile will be visited twice.
  38. # Will always see the centre.
  39. funcVisitTile(startX, startY)
  40. visited.add((startX, startY))
  41. # Ge the dimensions of the actual field of view, making
  42. # sure not to go off the map or beyond the radius.
  43. if startX < radius:
  44. minExtentX = startX
  45. else:
  46. minExtentX = radius
  47. if mapWidth - startX - 1 < radius:
  48. maxExtentX = mapWidth - startX - 1
  49. else:
  50. maxExtentX = radius
  51. if startY < radius:
  52. minExtentY = startY
  53. else:
  54. minExtentY = radius
  55. if mapHeight - startY - 1 < radius:
  56. maxExtentY = mapHeight - startY - 1
  57. else:
  58. maxExtentY = radius
  59. # Northeast quadrant
  60. __checkQuadrant(visited, startX, startY, 1, 1, \
  61. maxExtentX, maxExtentY, \
  62. funcVisitTile, funcTileBlocked)
  63. # Southeast quadrant
  64. __checkQuadrant(visited, startX, startY, 1, -1, \
  65. maxExtentX, minExtentY, \
  66. funcVisitTile, funcTileBlocked)
  67. # Southwest quadrant
  68. __checkQuadrant(visited, startX, startY, -1, -1, \
  69. minExtentX, minExtentY, \
  70. funcVisitTile, funcTileBlocked)
  71. # Northwest quadrant
  72. __checkQuadrant(visited, startX, startY, -1, 1, \
  73. minExtentX, maxExtentY, \
  74. funcVisitTile, funcTileBlocked)
  75. #-------------------------------------------------------------
  76. class __Line(object):
  77. def __init__(self, xi, yi, xf, yf):
  78. self.xi = xi
  79. self.yi = yi
  80. self.xf = xf
  81. self.yf = yf
  82. dx = property(fget = lambda self: self.xf - self.xi)
  83. dy = property(fget = lambda self: self.yf - self.yi)
  84. def pBelow(self, x, y):
  85. return self.relativeSlope(x, y) > 0
  86. def pBelowOrCollinear(self, x, y):
  87. return self.relativeSlope(x, y) >= 0
  88. def pAbove(self, x, y):
  89. return self.relativeSlope(x, y) < 0
  90. def pAboveOrCollinear(self, x, y):
  91. return self.relativeSlope(x, y) <= 0
  92. def pCollinear(self, x, y):
  93. return self.relativeSlope(x, y) == 0
  94. def lineCollinear(self, line):
  95. return self.pCollinear(line.xi, line.yi) \
  96. and self.pCollinear(line.xf, line.yf)
  97. def relativeSlope(self, x, y):
  98. return (self.dy * (self.xf - x)) \
  99. - (self.dx * (self.yf - y))
  100. class __ViewBump:
  101. def __init__(self, x, y, parent):
  102. self.x = x
  103. self.y = y
  104. self.parent = parent
  105. class __View:
  106. def __init__(self, shallowLine, steepLine):
  107. self.shallowLine = shallowLine
  108. self.steepLine = steepLine
  109. self.shallowBump = None
  110. self.steepBump = None
  111. def __checkQuadrant(visited, startX, startY, dx, dy, \
  112. extentX, extentY, funcVisitTile, funcTileBlocked):
  113. activeViews = []
  114. shallowLine = __Line(0, 1, extentX, 0)
  115. steepLine = __Line(1, 0, 0, extentY)
  116. activeViews.append( __View(shallowLine, steepLine) )
  117. viewIndex = 0
  118. # Visit the tiles diagonally and going outwards
  119. #
  120. # .
  121. # .
  122. # . .
  123. # 9 .
  124. # 5 8 .
  125. # 2 4 7
  126. # @ 1 3 6 . . .
  127. maxI = extentX + extentY
  128. i = 1
  129. while i != maxI + 1 and len(activeViews) > 0:
  130. if 0 > i - extentX:
  131. startJ = 0
  132. else:
  133. startJ = i - extentX
  134. if i < extentY:
  135. maxJ = i
  136. else:
  137. maxJ = extentY
  138. j = startJ
  139. while j != maxJ + 1 and viewIndex < len(activeViews):
  140. x = i - j
  141. y = j
  142. __visitCoord(visited, startX, startY, x, y, dx, dy, \
  143. viewIndex, activeViews, \
  144. funcVisitTile, funcTileBlocked)
  145. j += 1
  146. i += 1
  147. def __visitCoord(visited, startX, startY, x, y, dx, dy, viewIndex, \
  148. activeViews, funcVisitTile, funcTileBlocked):
  149. # The top left and bottom right corners of the current coordinate.
  150. topLeft = (x, y + 1)
  151. bottomRight = (x + 1, y)
  152. while viewIndex < len(activeViews) \
  153. and activeViews[viewIndex].steepLine.pBelowOrCollinear( \
  154. bottomRight[0], bottomRight[1]):
  155. # The current coordinate is above the current view and is
  156. # ignored. The steeper fields may need it though.
  157. viewIndex += 1
  158. if viewIndex == len(activeViews) \
  159. or activeViews[viewIndex].shallowLine.pAboveOrCollinear( \
  160. topLeft[0], topLeft[1]):
  161. # Either the current coordinate is above all of the fields
  162. # or it is below all of the fields.
  163. return
  164. # It is now known that the current coordinate is between the steep
  165. # and shallow lines of the current view.
  166. isBlocked = False
  167. # The real quadrant coordinates
  168. realX = x * dx
  169. realY = y * dy
  170. if (startX + realX, startY + realY) not in visited:
  171. visited.add((startX + realX, startY + realY))
  172. funcVisitTile(startX + realX, startY + realY)
  173. """else:
  174. # Debugging
  175. print (startX + realX, startY + realY)"""
  176. isBlocked = funcTileBlocked(startX + realX, startY + realY)
  177. if not isBlocked:
  178. # The current coordinate does not block sight and therefore
  179. # has no effect on the view.
  180. return
  181. if activeViews[viewIndex].shallowLine.pAbove( \
  182. bottomRight[0], bottomRight[1]) \
  183. and activeViews[viewIndex].steepLine.pBelow( \
  184. topLeft[0], topLeft[1]):
  185. # The current coordinate is intersected by both lines in the
  186. # current view. The view is completely blocked.
  187. del activeViews[viewIndex]
  188. elif activeViews[viewIndex].shallowLine.pAbove( \
  189. bottomRight[0], bottomRight[1]):
  190. # The current coordinate is intersected by the shallow line of
  191. # the current view. The shallow line needs to be raised.
  192. __addShallowBump(topLeft[0], topLeft[1], \
  193. activeViews, viewIndex)
  194. __checkView(activeViews, viewIndex)
  195. elif activeViews[viewIndex].steepLine.pBelow( \
  196. topLeft[0], topLeft[1]):
  197. # The current coordinate is intersected by the steep line of
  198. # the current view. The steep line needs to be lowered.
  199. __addSteepBump(bottomRight[0], bottomRight[1], activeViews, \
  200. viewIndex)
  201. __checkView(activeViews, viewIndex)
  202. else:
  203. # The current coordinate is completely between the two lines
  204. # of the current view. Split the current view into two views
  205. # above and below the current coordinate.
  206. shallowViewIndex = viewIndex
  207. viewIndex += 1
  208. steepViewIndex = viewIndex
  209. activeViews.insert(shallowViewIndex, \
  210. copy.deepcopy(activeViews[shallowViewIndex]))
  211. __addSteepBump(bottomRight[0], bottomRight[1], \
  212. activeViews, shallowViewIndex)
  213. if not __checkView(activeViews, shallowViewIndex):
  214. viewIndex -= 1
  215. steepViewIndex -= 1
  216. __addShallowBump(topLeft[0], topLeft[1], activeViews, \
  217. steepViewIndex)
  218. __checkView(activeViews, steepViewIndex)
  219. def __addShallowBump(x, y, activeViews, viewIndex):
  220. activeViews[viewIndex].shallowLine.xf = x
  221. activeViews[viewIndex].shallowLine.yf = y
  222. activeViews[viewIndex].shallowBump = __ViewBump(x, y, \
  223. activeViews[viewIndex].shallowBump)
  224. curBump = activeViews[viewIndex].steepBump
  225. while curBump is not None:
  226. if activeViews[viewIndex].shallowLine.pAbove( \
  227. curBump.x, curBump.y):
  228. activeViews[viewIndex].shallowLine.xi = curBump.x
  229. activeViews[viewIndex].shallowLine.yi = curBump.y
  230. curBump = curBump.parent
  231. def __addSteepBump(x, y, activeViews, viewIndex):
  232. activeViews[viewIndex].steepLine.xf = x
  233. activeViews[viewIndex].steepLine.yf = y
  234. activeViews[viewIndex].steepBump = __ViewBump(x, y, \
  235. activeViews[viewIndex].steepBump)
  236. curBump = activeViews[viewIndex].shallowBump
  237. while curBump is not None:
  238. if activeViews[viewIndex].steepLine.pBelow( \
  239. curBump.x, curBump.y):
  240. activeViews[viewIndex].steepLine.xi = curBump.x
  241. activeViews[viewIndex].steepLine.yi = curBump.y
  242. curBump = curBump.parent
  243. def __checkView(activeViews, viewIndex):
  244. """
  245. Removes the view in activeViews at index viewIndex if
  246. - The two lines are coolinear
  247. - The lines pass through either extremity
  248. """
  249. shallowLine = activeViews[viewIndex].shallowLine
  250. steepLine = activeViews[viewIndex].steepLine
  251. if shallowLine.lineCollinear(steepLine) \
  252. and ( shallowLine.pCollinear(0, 1) \
  253. or shallowLine.pCollinear(1, 0) ):
  254. del activeViews[viewIndex]
  255. return False
  256. else:
  257. return True