random_walk.gd 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162
  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 RandomWalkAgent
  6. enum State {FINISH, AVOID, IDLE, RANDOM, 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 action_weights: Dictionary = {"left": 30, "right": 30, "front": 40}
  11. func _ready() -> void:
  12. pass
  13. func _physics_process(delta: float) -> void:
  14. check_vision()
  15. match current_state:
  16. State.AVOID:
  17. avoid_collision()
  18. rotate_to(current_direction)
  19. set_state(State.WALK)
  20. State.FINISH:
  21. pass
  22. State.IDLE:
  23. pass
  24. State.RANDOM:
  25. var new_direction = perform_random_rotate_action(current_direction)
  26. change_direction(new_direction)
  27. set_state(State.WALK)
  28. State.UPDATE:
  29. # Check if agent is done exploring
  30. if map.explored():
  31. exploration.stop()
  32. update_agent_state()
  33. self.finished.emit(agent_state)
  34. set_state(State.FINISH)
  35. else:
  36. update_agent_state()
  37. set_state(State.RANDOM)
  38. State.WALK:
  39. rotate_to(current_direction)
  40. var new_velocity : Vector2 = current_direction * speed
  41. velocity = new_velocity
  42. move_and_slide()
  43. func setup():
  44. build_empty_map()
  45. exploration.set_wait_time(4)
  46. exploration.start()
  47. var new_direction = generate_patrol_direction()
  48. #var new_direction = Vector2i(-1, 0)
  49. change_direction(new_direction)
  50. init_agent_state()
  51. set_state(State.WALK)
  52. func get_state() -> int:
  53. return current_state
  54. func set_state(new_state: int):
  55. if current_state == new_state:
  56. return # early exit if no change needed
  57. previous_state = current_state
  58. current_state = new_state
  59. func change_direction(direction: Vector2):
  60. var current_map_tile = map.global_to_tile(global_position)
  61. current_direction = direction
  62. func avoid_collision():
  63. stop_agent()
  64. var new_direction: Vector2
  65. if randi_range(0, 4):
  66. new_direction = utils.rotate_vec_right(current_direction)
  67. else:
  68. new_direction = utils.rotate_vec_left(current_direction)
  69. change_direction(new_direction)
  70. func check_vision():
  71. # Awaiting the SceneTree's process_frame signal. Otherwise, the rays can be cast without
  72. # having any objects to collide with in the very first frame. This is caused by the
  73. # TileMapLayer is not fully set up yet.
  74. await get_tree().process_frame
  75. for ray: RayCast2D in vision.get_children():
  76. var seen_tiles: Array[Vector2i] = []
  77. if ray.is_colliding():
  78. var collision = round(ray.get_collision_point())
  79. if global_position.distance_to(collision) <= 24.0:
  80. set_state(State.AVOID)
  81. # Collisions with other agents are not marked on the map as obstacles
  82. if ray.get_collider() is BehaviorAgent:
  83. continue
  84. # Ignore collision when colliding with a corner of a map tile. The normal at the point
  85. # of collision is not defined and results in unexpected map updates.
  86. if not int(collision.x) % map._tile_size and not int(collision.y) % map._tile_size:
  87. continue
  88. # We use the collision normal correct the position of the collision before translating
  89. # the coordinates to the map grid.
  90. var normal = ray.get_collision_normal()
  91. # Mark the colliding tile as OBSTACLE (1)
  92. var collision_tile = map.global_to_tile(collision - normal)
  93. map.set_tile_value(collision_tile, 1)
  94. seen_tiles = map.dda_ray_tiles(ray.global_position, collision + normal)
  95. else:
  96. seen_tiles = map.dda_ray_tiles(ray.global_position, to_global(ray.target_position))
  97. for tile in seen_tiles:
  98. map.set_tile_value(tile, 0)
  99. func generate_patrol_direction():
  100. var patrol_x = randi_range(-1, 1)
  101. var patrol_y = randi_range(-1, 1)
  102. var direction
  103. if patrol_x:
  104. direction = Vector2(patrol_x, 0)
  105. elif patrol_y:
  106. direction = Vector2(0, patrol_y)
  107. else:
  108. direction = Vector2(1, 0)
  109. return direction
  110. func perform_random_rotate_action(direction: Vector2) -> Vector2:
  111. var total_weight = 100.0
  112. var threshold = randf_range(0.0, total_weight)
  113. var accumulated_weight = 0.0
  114. var actions = action_weights.keys()
  115. var pick = ""
  116. for action in actions:
  117. accumulated_weight += action_weights[action]
  118. if accumulated_weight >= threshold:
  119. pick = action
  120. break
  121. if pick == "left":
  122. return utils.rotate_vec_left(direction)
  123. if pick == "right":
  124. return utils.rotate_vec_right(direction)
  125. if pick == "front":
  126. return direction
  127. return direction
  128. func _on_exploration_timeout() -> void:
  129. stop_agent()
  130. set_state(State.UPDATE)
  131. #print(map.stringify_grid2d())