Grid2D.gd 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258
  1. class_name Grid2D
  2. extends RefCounted
  3. var NEIGHBOR_DIRECTIONS = [Vector2i(1,0), Vector2i(0,1), Vector2i(-1,0), Vector2i(0,-1)]
  4. var _data: Array[Grid2DTile]
  5. var _size: Vector2i
  6. var _tile_size: int
  7. var _grid_origin: Vector2
  8. func _init(size: Vector2i, tile_size: int, origin: Vector2, start_pos: Vector2, data: Array = []) -> void:
  9. if not data.is_empty():
  10. assert(data.size() == size.x * size.y, "Given data does not match the specified size.")
  11. if not data.is_empty() and data.size() == size.x * size.y:
  12. self._data = data
  13. else:
  14. self._data = []
  15. self._size = size
  16. self._tile_size = tile_size
  17. self._grid_origin = origin
  18. # Fill all tiles of the map in with UNKOWN (-1).
  19. var cell_count = _size.x * _size.y
  20. for i in cell_count:
  21. var x: int = i % self._size.x
  22. var y: int = i / self._size.x
  23. var tile: Grid2DTile = Grid2DTile.new(x, y, -1)
  24. _data.append(tile)
  25. # Fill in the starting position as FREE (0). The agent using this map will start exploring from this position.
  26. self.set_tile_value(self.global_to_tile(start_pos), 0)
  27. func get_tile(pos: Vector2i) -> Grid2DTile:
  28. var index = pos.x + (pos.y * self._size.x)
  29. return self._data[index]
  30. func set_tile_value(pos: Vector2i, value: int) -> void:
  31. var index = pos.x + (pos.y * self._size.x)
  32. if index < 0 or index >= self._data.size():
  33. return
  34. self._data[index].val = value
  35. func update_map(data: Dictionary, ray_origin: Vector2) -> void:
  36. var collisions: Array[Vector2] = data["collisions"]
  37. var normals: Array[Vector2] = data["normals"]
  38. for i in range(collisions.size()):
  39. var free_tiles = self.dda_ray_tiles(ray_origin, collisions[i] + normals[i])
  40. for tile in free_tiles:
  41. self.set_tile_value(tile, 0)
  42. if normals[i] != Vector2.ZERO:
  43. var obstacle_tile = self.global_to_tile(collisions[i] - normals[i])
  44. self.set_tile_value(obstacle_tile, 1)
  45. func manhattan_distance(a: Vector2i, b: Vector2i):
  46. return abs(a.x - b.x) + abs(a.y - b.y)
  47. func global_to_tile(global_pos: Vector2) -> Vector2i:
  48. # TODO check if global_pos is inside the bounds of this grid
  49. var instance_position = global_pos - self._grid_origin
  50. var tile_x: int = floori(instance_position.x / self._tile_size)
  51. var tile_y: int = floori(instance_position.y / self._tile_size)
  52. return Vector2i(tile_x, tile_y)
  53. func tile_to_global(grid_pos: Vector2i) -> Vector2:
  54. # TODO check if given grid position is within the bounds of this grid
  55. var global_x: float = self._grid_origin.x + (grid_pos.x * self._tile_size) + (self._tile_size/2)
  56. var global_y: float = self._grid_origin.y + (grid_pos.y * self._tile_size) + (self._tile_size/2)
  57. return Vector2(global_x, global_y)
  58. func path_to_global(path: Array[Vector2i]) -> Array[Vector2]:
  59. var global_path: Array[Vector2] = []
  60. for waypoint in path:
  61. global_path.append(self.tile_to_global(waypoint))
  62. return global_path
  63. func explored() -> bool:
  64. return self.get_frontier_tiles().is_empty()
  65. func get_neighbor_values(pos: Vector2i) -> Dictionary[Vector2i, int]:
  66. var values: Dictionary[Vector2i, int] = {}
  67. for direction in NEIGHBOR_DIRECTIONS:
  68. var neighbor: Vector2i = pos + direction
  69. if 0 <= neighbor.x and neighbor.x < self._size.x and 0 <= neighbor.y and neighbor.y < self._size.y:
  70. values[neighbor] = self.get_tile(neighbor).val
  71. return values
  72. func get_frontier_tiles() -> Array[Vector2i]:
  73. var frontier: Array[Vector2i] = []
  74. for y in self._size.y:
  75. for x in self._size.x:
  76. var pos: Vector2i = Vector2i(x, y)
  77. if self.get_tile(pos).val == 0 and self.get_neighbor_values(pos).values().find(-1) != -1:
  78. frontier.append(pos)
  79. return frontier
  80. func compute_utility(pos: Vector2i, agent_pos: Vector2i, radius: int = 2) -> float:
  81. # TODO update utility calculation
  82. # NOW: just counting the amount of unknown tiles in a radius around the given tile position
  83. # IDEAS:
  84. # take position (and/or direction) of the agent into acount ( higher utility for closer tiles and in same direction )
  85. # keep count of number of visits to a tile. penalize utility for visiting a tile very often ( fresh explore )
  86. var count = 0
  87. for dx in range(-radius, radius+1):
  88. for dy in range(-radius, radius+1):
  89. var neighbor: Vector2i = pos + Vector2i(dx, dy)
  90. if 0 <= neighbor.x and neighbor.x < self._size.x and 0 <= neighbor.y and neighbor.y < self._size.y and self.get_tile(neighbor).val == -1:
  91. count += 2
  92. var utility = count
  93. var distance = self.manhattan_distance(agent_pos, pos)
  94. if distance > 0:
  95. utility -= distance
  96. return utility
  97. func get_frontier_utilities(agent_pos: Vector2i) -> MaxHeap:
  98. var frontier_utilities: MaxHeap = MaxHeap.new()
  99. var frontier_tiles: Array[Vector2i] = self.get_frontier_tiles()
  100. for tile in frontier_tiles:
  101. var utility: float = self.compute_utility(tile, agent_pos)
  102. var heap_node: HeapNode = HeapNode.new(tile, utility)
  103. frontier_utilities.push(heap_node)
  104. return frontier_utilities
  105. func new_exploration_target(position: Vector2):
  106. var current: Vector2i = self.global_to_tile(position)
  107. var frontier: MaxHeap = self.get_frontier_utilities(current)
  108. if not frontier.empty():
  109. var target: Vector2i = frontier.pop().pos
  110. # If the first returned target is the same as the current tile, we take the next best tile
  111. # in the frontier.
  112. if target == current and not frontier.empty():
  113. return frontier.pop().pos
  114. elif target == current and frontier.empty():
  115. return null
  116. return target
  117. else:
  118. return null
  119. func compare(grid: Grid2D) -> void:
  120. for i in range(self._data.size()):
  121. if self._data[i].val == -1 and grid._data[i].val != -1:
  122. self._data[i].val = grid._data[i].val
  123. func fill_enclosed_unknown_regions() -> void:
  124. var visited: Dictionary = {}
  125. for y in self._size.y:
  126. for x in self._size.x:
  127. var pos: Vector2i = Vector2i(x, y)
  128. if self.get_tile(pos).val == -1 and pos not in visited.keys():
  129. var region: Array = []
  130. var is_enclosed: bool = true
  131. var queue: Array = [pos]
  132. # Flood fill loop
  133. while not queue.is_empty():
  134. var current: Vector2i = queue.pop_front()
  135. if current in visited.keys():
  136. continue
  137. visited[current] = true
  138. region.append(current)
  139. for direction in NEIGHBOR_DIRECTIONS:
  140. var neighbor: Vector2i = current + direction
  141. # Grid Bounds Check
  142. if 0 <= neighbor.x and neighbor.x < self._size.x and 0 <= neighbor.y and neighbor.y < self._size.y:
  143. if self.get_tile(neighbor).val == -1 and neighbor not in visited.keys():
  144. queue.push_back(neighbor)
  145. elif self.get_tile(neighbor).val == 0:
  146. # Connected to FREE (0) tile => region is not enclosed
  147. is_enclosed = false
  148. # If the region is enclosed, mark all tiles in the region as OBSTACLE (1)
  149. if is_enclosed:
  150. for el in region:
  151. self.set_tile_value(el, 1)
  152. func dda_ray_tiles(global_start: Vector2, global_end: Vector2) -> Array[Vector2i]:
  153. var tiles: Array[Vector2i] = []
  154. var start: Vector2 = (global_start - self._grid_origin) / self._tile_size
  155. var end: Vector2 = (global_end - self._grid_origin) / self._tile_size
  156. var dx = end.x - start.x
  157. var dy = end.y - start.y
  158. var step_x = 1 if dx > 0 else -1
  159. var step_y = 1 if dy > 0 else -1
  160. var tile = self.global_to_tile(global_start)
  161. var end_tile = self.global_to_tile(global_end)
  162. var tile_corr_x = 1 if step_x > 0 else 0
  163. var tile_corr_y = 1 if step_y > 0 else 0
  164. var t_max_x = ((tile.x + tile_corr_x) - start.x) / dx if dx !=0 else INF
  165. var t_max_y = ((tile.y + tile_corr_y) - start.y) / dy if dy !=0 else INF
  166. var t_delta_x = abs(1/dx) if dx !=0 else INF
  167. var t_delta_y = abs(1/dy) if dy !=0 else INF
  168. var max_steps = 100
  169. for step in max_steps:
  170. tiles.append(tile)
  171. if t_max_x < t_max_y:
  172. t_max_x += t_delta_x
  173. tile.x += step_x
  174. else:
  175. t_max_y += t_delta_y
  176. tile.y += step_y
  177. if (
  178. (step_x > 0 and tile.x > end_tile.x) or
  179. (step_x < 0 and tile.x < end_tile.x) or
  180. (step_y > 0 and tile.y > end_tile.y) or
  181. (step_y < 0 and tile.y < end_tile.y)
  182. ):
  183. break
  184. return tiles
  185. func stringify_grid2d() -> String:
  186. var map_string = ""
  187. for y in self._size.y:
  188. for x in self._size.x:
  189. var entry = get_tile(Vector2i(x, y))
  190. if entry.val == 1:
  191. map_string += "x"
  192. elif entry.val == 0:
  193. map_string += " "
  194. elif entry.val == -1:
  195. map_string += "?"
  196. map_string += "\n"
  197. return map_string
  198. class Grid2DTile:
  199. var val: int
  200. var pos: Vector2i
  201. func _init(x: int, y: int, value: int) -> void:
  202. self.val = value
  203. self.pos = Vector2i(x, y)