Guest User

Untitled

a guest
Jul 18th, 2026
40
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 9.20 KB | None | 0 0
  1. @icon("res://addons/at-icons/node3d/steering_wheel.svg")
  2. class_name Steering
  3. extends Node3D
  4.  
  5. signal fallback_direction_used
  6.  
  7. enum DangerComputationMethod { RAY, MULTIRAY, SHAPE }
  8.  
  9. @export var body: CharacterBody3D
  10. @export var gravity: bool = true
  11. @export_category("Movement")
  12. @export_range(1, 10, 0.1, "suffix:m/s") var movement_speed: float = 4.0
  13. @export_range(0.5, 15, 0.1, "How many times per second the gap to target gets closed") var steering_speed: float = 0.2
  14. ## Apply forces to keep bodies apart
  15. @export_category("Separation")
  16. @export var separation_radius: float = 1.0
  17. @export var separation_weight: float = 0.0
  18. @export_category("Context Aware Steering")
  19. @export var danger_method: DangerComputationMethod = DangerComputationMethod.RAY
  20. @export var collision_shape: Shape3D
  21. @export var ray_count: int = 16:
  22. set = set_ray_count
  23. @export var look_ahead: float = 2.0
  24. @export var ray_origin_offset: Vector3 = Vector3.UP
  25. @export var steering_bias_seed: float = randf_range(-1.0, 1.0)
  26. @export_group("Validation", "validation_")
  27. ## Project the actual body shape towards the chosen direction and validate any collision.
  28. @export var validation_use_body_shape: bool = true
  29. @export_category("Modifiers")
  30. @export var _modifiers: Array[SpeedModifier] = []
  31. @export_category("Debug")
  32. @export var debug_draw_enabled := false:
  33. set(value):
  34. debug_draw_enabled = value
  35. if not value and _debug_draw:
  36. _debug_draw.clear()
  37.  
  38. var current_velocity: Vector3 = Vector3.ZERO
  39. var _ray_directions: Array[Vector3] = []
  40. var _interest: Array[float] = []
  41. var _danger: Array[float] = []
  42. var _last_chosen_direction := Vector3.MODEL_FRONT
  43.  
  44. @onready var _debug_draw: SteeringDebugDraw = $MeshInstance3D
  45. @onready var area_3d: Area3D = $Area3D
  46.  
  47.  
  48. func _ready() -> void:
  49. if not body:
  50. body = get_parent()
  51. set_ray_count(ray_count)
  52.  
  53.  
  54. func update(delta: float, target_position: Vector3) -> Vector3:
  55. _modifiers = _modifiers.filter(func(mod: SpeedModifier) -> bool: return mod.tick(delta))
  56.  
  57. var direction := target_position - body.global_position
  58.  
  59. _compute_interest(direction)
  60. _compute_danger()
  61.  
  62. var chosen_direction := _choose_direction(direction)
  63. if validation_use_body_shape:
  64. chosen_direction = _validate_chosen_direction(chosen_direction)
  65.  
  66. var effective_speed := _get_effective_speed()
  67. var separation_force := get_separation_force(area_3d.get_overlapping_bodies(), separation_radius)
  68.  
  69. var desired_velocity := chosen_direction.normalized() * effective_speed
  70. current_velocity = current_velocity.lerp(desired_velocity, 1.0 - exp(-steering_speed * delta))
  71. current_velocity += separation_force * separation_weight
  72. current_velocity += body.get_gravity() * delta
  73.  
  74. if debug_draw_enabled and _debug_draw:
  75. _debug_draw.draw_steering(
  76. _ray_directions,
  77. _interest,
  78. _danger,
  79. chosen_direction,
  80. direction,
  81. look_ahead,
  82. )
  83.  
  84. return current_velocity
  85.  
  86.  
  87. func get_separation_force(neighbors: Array[Node3D], radius: float) -> Vector3:
  88. var force := Vector3.ZERO
  89. for neighbor in neighbors:
  90. var dist := body.global_position.distance_to(neighbor.global_position)
  91. if dist < radius and dist > 0.01:
  92. var away := (body.global_position - neighbor.global_position).normalized()
  93. force += away * (1.0 - dist / radius) # stronger when closer
  94. force.y = 0
  95. return force.normalized()
  96.  
  97.  
  98. func add_modifier(modifier: SpeedModifier) -> void:
  99. _modifiers.append(modifier)
  100.  
  101.  
  102. func remove_modifier(modifier: SpeedModifier) -> void:
  103. _modifiers.erase(modifier)
  104.  
  105.  
  106. func set_ray_count(value: int) -> void:
  107. ray_count = value
  108.  
  109. _ray_directions.resize(ray_count)
  110.  
  111. var half_step := PI / ray_count # offset so no ray sits at exactly forward/back
  112. for i in ray_count:
  113. var angle := i * 2 * PI / ray_count + half_step
  114. _ray_directions[i] = Vector3.MODEL_FRONT.rotated(Vector3.UP, angle).normalized()
  115.  
  116.  
  117. func _compute_interest(desired_direction: Vector3) -> void:
  118. _interest.resize(ray_count)
  119. for i in ray_count:
  120. var d := _ray_directions[i].dot(desired_direction)
  121. # To prevent left/right stalemates when obstacles straight ahead
  122. var bias := _ray_directions[i].cross(Vector3.UP).dot(Vector3.MODEL_FRONT) \
  123. * steering_bias_seed * 0.05
  124.  
  125. _interest[i] = max(0, d + bias)
  126.  
  127.  
  128. func _compute_danger() -> void:
  129. _danger.resize(ray_count)
  130. match danger_method:
  131. DangerComputationMethod.RAY:
  132. _compute_danger_ray_cast()
  133. DangerComputationMethod.MULTIRAY:
  134. _compute_danger_multi_ray()
  135. DangerComputationMethod.SHAPE:
  136. _compute_danger_shape_cast()
  137.  
  138.  
  139. func _compute_danger_ray_cast() -> void:
  140. var origin := body.global_position + ray_origin_offset
  141. # Cast rays to find danger directions
  142. var space_state := get_world_3d().direct_space_state
  143. for i in ray_count:
  144. var target := origin + _ray_directions[i] * look_ahead
  145. var query := PhysicsRayQueryParameters3D.create(
  146. origin,
  147. target,
  148. Globals.collision_mask_units,
  149. [body],
  150. )
  151. var result := space_state.intersect_ray(query)
  152. if result.is_empty():
  153. _danger[i] = 0.0
  154. else:
  155. @warning_ignore("unsafe_call_argument")
  156. var hit_dist := origin.distance_to(result.position)
  157. _danger[i] = 1.0 - clampf(hit_dist / look_ahead, 0.0, 1.0)
  158. #_danger[i] = 1.0
  159.  
  160.  
  161. # Doesn't seem to work that well
  162. func _compute_danger_multi_ray() -> void:
  163. var space_state := get_world_3d().direct_space_state
  164. var origin := body.global_position + Vector3.UP
  165.  
  166. for i in ray_count:
  167. var dir := _ray_directions[i]
  168. var side := dir.cross(Vector3.UP).normalized() * 0.4
  169. var max_danger := 0.0
  170.  
  171. for offset: Vector3 in [Vector3.ZERO, side, -side]: # center, left edge, right edge
  172. var o := origin + ray_origin_offset
  173. var query := PhysicsRayQueryParameters3D.create(
  174. o,
  175. o + dir * look_ahead,
  176. Globals.collision_mask_units,
  177. [body],
  178. )
  179. var result := space_state.intersect_ray(query)
  180. if not result.is_empty():
  181. @warning_ignore("unsafe_call_argument")
  182. var hit_dist := o.distance_to(result.position)
  183. max_danger = max(max_danger, 1.0 - clampf(hit_dist / look_ahead, 0.0, 1.0))
  184. #max_danger = 1.0
  185. _danger[i] = max_danger
  186.  
  187.  
  188. func _compute_danger_shape_cast() -> void:
  189. var origin := body.global_position + ray_origin_offset
  190.  
  191. for i in ray_count:
  192. var result := _shape_cast_body(origin, _ray_directions[i])
  193. # result[0] = safe fraction of motion before collision, result[1] = fraction at contact
  194. var hit_fraction: float = result[0] if result.size() > 0 else 1.0
  195. _danger[i] = 1.0 - hit_fraction
  196.  
  197.  
  198. func _choose_direction(original_direction: Vector3) -> Vector3:
  199. # Eliminate interest in slots with danger
  200. for i in ray_count:
  201. if _danger[i] > 0.0:
  202. _interest[i] = 0.0
  203. #_interest[i] = max(0.0, _interest[i] - _danger[i])
  204. else:
  205. # small continuity bonus for directions close to what we picked last frame
  206. _interest[i] += max(0.0, _ray_directions[i].dot(_last_chosen_direction)) * 0.15
  207.  
  208. # Choose direction based on remaining interest
  209. var chosen_dir := Vector3.ZERO
  210. for i in ray_count:
  211. chosen_dir += _ray_directions[i] * _interest[i]
  212.  
  213. if chosen_dir.is_zero_approx():
  214. return _fallback_direction(original_direction)
  215.  
  216. _last_chosen_direction = chosen_dir
  217. return chosen_dir.normalized()
  218.  
  219.  
  220. ## Check if the chosen direction is actually valid based on the actual shape
  221. ## of the agent/body. Cheaper than converting rays
  222. func _validate_chosen_direction(chosen_dir: Vector3) -> Vector3:
  223. @warning_ignore("unsafe_call_argument")
  224. var result := _shape_cast_body(
  225. body.global_position + ray_origin_offset,
  226. chosen_dir,
  227. min(look_ahead, 1.0), # short-range
  228. )
  229. if result.size() > 0 and result[0] < 0.5:
  230. # chosen direction is actually blocked for our body => fallback
  231. return _fallback_direction(chosen_dir)
  232. return chosen_dir
  233.  
  234.  
  235. func _shape_cast_body(
  236. origin: Vector3,
  237. direction: Vector3,
  238. look_ahead_: float = look_ahead,
  239. ) -> PackedFloat32Array:
  240. var space_state := get_world_3d().direct_space_state
  241. var shape: Shape3D
  242. if collision_shape:
  243. shape = collision_shape
  244. else:
  245. var capsule_shape := CapsuleShape3D.new()
  246. capsule_shape.radius = 0.4
  247. capsule_shape.height = 1.8
  248. shape = capsule_shape
  249.  
  250. var query := PhysicsShapeQueryParameters3D.new()
  251. query.shape = shape
  252. query.transform = Transform3D(Basis(), origin)
  253. query.motion = direction * look_ahead_
  254. query.collision_mask = Globals.collision_mask_units
  255. query.exclude = [body]
  256. return space_state.cast_motion(query)
  257.  
  258.  
  259. func _fallback_direction(_original_direction: Vector3) -> Vector3:
  260. #return _last_chosen_direction
  261.  
  262. #var left_right := randi() % 2 # 0 or 1
  263. #var base_angle := left_right * PI + PI / 2 # -PI/2 for right PI/2 for left
  264. #var angle := base_angle + randf_range(deg_to_rad(-10), deg_to_rad(10))
  265. #return original_direction.rotated(Vector3.UP, angle)
  266.  
  267. # find the ray with lowest danger; if tied, prefer the one closest to last direction
  268. var best_i := -1
  269. var best_danger := INF
  270. for i in ray_count:
  271. var d: float = _danger[i]
  272. if d < best_danger:
  273. best_danger = d
  274. best_i = i
  275. return _ray_directions[best_i] if best_i != -1 else -_last_chosen_direction
  276.  
  277.  
  278. func _get_effective_speed() -> float:
  279. var total_multiplier := 1.0
  280. var total_flat := 0.0
  281. for mod in _modifiers:
  282. total_multiplier *= mod.multiplier
  283. total_flat += mod.flat_bonus
  284. return movement_speed * total_multiplier + total_flat
  285.  
Add Comment
Please, Sign In to add comment