exploration.gd 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159
  1. # This code is based on work by Joshua Moelans
  2. # https://github.com/JoshuaMoelans/Master-Thesis-Godot-exploration (accessed March 2025)
  3. # Originally developed for his master's thesis at the University of Antwerp
  4. extends BehaviorAgent
  5. class_name ExplorationAgent
  6. enum State {FINISH, AVOID, EXPLORE, IDLE, UPDATE, WALK}
  7. @onready var exploration: Timer = $Exploration
  8. var current_state: int = -1 : set = set_state, get = get_state
  9. var previous_state: int = -1
  10. var pathfinding: AStar = AStar.new()
  11. var exploration_path: Array[Vector2i] = []
  12. func _ready() -> void:
  13. pass
  14. func _physics_process(delta: float) -> void:
  15. check_vision()
  16. match current_state:
  17. State.AVOID:
  18. avoid_collision()
  19. rotate_to(current_direction)
  20. set_state(State.WALK)
  21. State.EXPLORE:
  22. var start = map.global_to_tile(global_position)
  23. var goal = new_exploration_target(start)
  24. if goal == null:
  25. set_state(State.IDLE)
  26. else:
  27. exploration_path = pathfinding.astar(map, start, goal)
  28. set_state(State.WALK)
  29. State.FINISH:
  30. pass
  31. State.IDLE:
  32. pass
  33. State.UPDATE:
  34. # Check if agent is done exploring
  35. if map.explored():
  36. exploration.stop()
  37. update_agent_state()
  38. self.finished.emit(agent_state)
  39. set_state(State.FINISH)
  40. else:
  41. update_agent_state()
  42. set_state(State.WALK)
  43. State.WALK:
  44. if not exploration_path.is_empty():
  45. if global_position.distance_to(map.tile_to_global(exploration_path[0])) <= 24.0:
  46. stop_agent()
  47. exploration_path.pop_front()
  48. else:
  49. var next_path_position = map.tile_to_global(exploration_path[0])
  50. var new_direction = global_position.direction_to(next_path_position)
  51. change_direction(new_direction)
  52. rotate_to(current_direction)
  53. var new_velocity : Vector2 = current_direction * speed
  54. velocity = new_velocity
  55. move_and_slide()
  56. else:
  57. stop_agent()
  58. set_state(State.EXPLORE)
  59. func setup():
  60. build_empty_map()
  61. exploration.set_wait_time(5)
  62. exploration.start()
  63. var new_direction = Vector2(1, 0)
  64. change_direction(new_direction)
  65. init_agent_state()
  66. set_state(State.WALK)
  67. func get_state() -> int:
  68. return current_state
  69. func set_state(new_state: int):
  70. if current_state == new_state:
  71. return # early exit if no change needed
  72. previous_state = current_state
  73. current_state = new_state
  74. func get_tile_neighbors(pos: Vector2i) -> Array[Vector2i]:
  75. var directions = [Vector2i(1,0), Vector2i(0,1), Vector2i(-1,0), Vector2i(0,-1)]
  76. var neighbors = []
  77. for vec in directions:
  78. var neighbor = pos + vec
  79. if neighbor.x < map._size.x and neighbor.y < map._size.y:
  80. neighbors.append(neighbor)
  81. return neighbors
  82. func change_direction(direction: Vector2):
  83. var current_map_tile = map.global_to_tile(global_position)
  84. current_direction = direction
  85. func check_vision():
  86. # Awaiting the SceneTree's process_frame signal. Otherwise, the rays can be cast without
  87. # having any objects to collide with in the very first frame. This is caused by the
  88. # TileMapLayer is not fully set up yet.
  89. await get_tree().process_frame
  90. for ray in vision.get_children():
  91. var seen_tiles: Array[Vector2i] = []
  92. if ray.is_colliding():
  93. var collision = ray.get_collision_point()
  94. if global_position.distance_to(collision) <= 24.0:
  95. set_state(State.AVOID)
  96. # Collisions with other agents are not marked on the map as obstacles
  97. if ray.get_collider() is BehaviorAgent:
  98. continue
  99. # Ignore collision when colliding with a corner of a map tile. The normal at the point
  100. # of collision is not defined and gives unexpected map updates.
  101. if not int(collision.x) % map._tile_size and not int(collision.y) % map._tile_size:
  102. continue
  103. # We use the collision normal correct the position of the collision before translating
  104. # the coordinates to the map grid.
  105. var normal = ray.get_collision_normal()
  106. # Mark the colliding tile as OBSTACLE (1)
  107. var collision_tile = map.global_to_tile(collision - normal)
  108. map.set_tile_value(collision_tile, 1)
  109. seen_tiles = map.dda_ray_tiles(ray.global_position, collision + normal)
  110. else:
  111. seen_tiles = map.dda_ray_tiles(ray.global_position, to_global(ray.target_position))
  112. for tile in seen_tiles:
  113. map.set_tile_value(tile, 0)
  114. func new_exploration_target(current: Vector2i):
  115. var frontier: MaxHeap = map.get_frontier_utilities(current)
  116. if not frontier.empty():
  117. var target: Vector2i = frontier.pop().pos
  118. # If the first returned target is the same as the current tile, we take the next best tile
  119. # in the frontier.
  120. if target == current and not frontier.empty():
  121. return frontier.pop().pos
  122. elif target == current and frontier.empty():
  123. return null
  124. return target
  125. else:
  126. return null
  127. func _on_exploration_timeout() -> void:
  128. set_state(State.UPDATE)