Added GDQuest's steering framework.

This commit is contained in:
Relintai 2020-02-19 13:06:40 +01:00
parent a1fd74a34a
commit b14866b143
33 changed files with 1649 additions and 0 deletions

View File

@ -39,6 +39,151 @@ _global_script_classes=[ {
"language": "GDScript",
"path": "res://voxelman/world/CubicVoxelMesher.gd"
}, {
"base": "Reference",
"class": "GSAIAgentLocation",
"language": "GDScript",
"path": "res://steering_ai_framework/GSAIAgentLocation.gd"
}, {
"base": "GSAISteeringBehavior",
"class": "GSAIArrive",
"language": "GDScript",
"path": "res://steering_ai_framework/Behaviors/GSAIArrive.gd"
}, {
"base": "GSAIGroupBehavior",
"class": "GSAIAvoidCollisions",
"language": "GDScript",
"path": "res://steering_ai_framework/Behaviors/GSAIAvoidCollisions.gd"
}, {
"base": "GSAISteeringBehavior",
"class": "GSAIBlend",
"language": "GDScript",
"path": "res://steering_ai_framework/Behaviors/GSAIBlend.gd"
}, {
"base": "GSAIGroupBehavior",
"class": "GSAICohesion",
"language": "GDScript",
"path": "res://steering_ai_framework/Behaviors/GSAICohesion.gd"
}, {
"base": "GSAIPursue",
"class": "GSAIEvade",
"language": "GDScript",
"path": "res://steering_ai_framework/Behaviors/GSAIEvade.gd"
}, {
"base": "GSAIMatchOrientation",
"class": "GSAIFace",
"language": "GDScript",
"path": "res://steering_ai_framework/Behaviors/GSAIFace.gd"
}, {
"base": "GSAISeek",
"class": "GSAIFlee",
"language": "GDScript",
"path": "res://steering_ai_framework/Behaviors/GSAIFlee.gd"
}, {
"base": "GSAIArrive",
"class": "GSAIFollowPath",
"language": "GDScript",
"path": "res://steering_ai_framework/Behaviors/GSAIFollowPath.gd"
}, {
"base": "GSAISteeringBehavior",
"class": "GSAIGroupBehavior",
"language": "GDScript",
"path": "res://steering_ai_framework/GSAIGroupBehavior.gd"
}, {
"base": "GSAIProximity",
"class": "GSAIInfiniteProximity",
"language": "GDScript",
"path": "res://steering_ai_framework/Proximities/GSAIInfiniteProximity.gd"
}, {
"base": "GSAISpecializedAgent",
"class": "GSAIKinematicBody2DAgent",
"language": "GDScript",
"path": "res://steering_ai_framework/Agents/GSAIKinematicBody2DAgent.gd"
}, {
"base": "GSAISpecializedAgent",
"class": "GSAIKinematicBody3DAgent",
"language": "GDScript",
"path": "res://steering_ai_framework/Agents/GSAIKinematicBody3DAgent.gd"
}, {
"base": "GSAIMatchOrientation",
"class": "GSAILookWhereYouGo",
"language": "GDScript",
"path": "res://steering_ai_framework/Behaviors/GSAILookWhereYouGo.gd"
}, {
"base": "GSAISteeringBehavior",
"class": "GSAIMatchOrientation",
"language": "GDScript",
"path": "res://steering_ai_framework/Behaviors/GSAIMatchOrientation.gd"
}, {
"base": "Reference",
"class": "GSAIPath",
"language": "GDScript",
"path": "res://steering_ai_framework/GSAIPath.gd"
}, {
"base": "GSAISteeringBehavior",
"class": "GSAIPriority",
"language": "GDScript",
"path": "res://steering_ai_framework/Behaviors/GSAIPriority.gd"
}, {
"base": "Reference",
"class": "GSAIProximity",
"language": "GDScript",
"path": "res://steering_ai_framework/Proximities/GSAIProximity.gd"
}, {
"base": "GSAISteeringBehavior",
"class": "GSAIPursue",
"language": "GDScript",
"path": "res://steering_ai_framework/Behaviors/GSAIPursue.gd"
}, {
"base": "GSAIProximity",
"class": "GSAIRadiusProximity",
"language": "GDScript",
"path": "res://steering_ai_framework/Proximities/GSAIRadiusProximity.gd"
}, {
"base": "GSAISpecializedAgent",
"class": "GSAIRigidBody2DAgent",
"language": "GDScript",
"path": "res://steering_ai_framework/Agents/GSAIRigidBody2DAgent.gd"
}, {
"base": "GSAISpecializedAgent",
"class": "GSAIRigidBody3DAgent",
"language": "GDScript",
"path": "res://steering_ai_framework/Agents/GSAIRigidBody3DAgent.gd"
}, {
"base": "GSAISteeringBehavior",
"class": "GSAISeek",
"language": "GDScript",
"path": "res://steering_ai_framework/Behaviors/GSAISeek.gd"
}, {
"base": "GSAIGroupBehavior",
"class": "GSAISeparation",
"language": "GDScript",
"path": "res://steering_ai_framework/Behaviors/GSAISeparation.gd"
}, {
"base": "GSAISteeringAgent",
"class": "GSAISpecializedAgent",
"language": "GDScript",
"path": "res://steering_ai_framework/Agents/GSAISpecializedAgent.gd"
}, {
"base": "GSAIAgentLocation",
"class": "GSAISteeringAgent",
"language": "GDScript",
"path": "res://steering_ai_framework/GSAISteeringAgent.gd"
}, {
"base": "Reference",
"class": "GSAISteeringBehavior",
"language": "GDScript",
"path": "res://steering_ai_framework/GSAISteeringBehavior.gd"
}, {
"base": "Reference",
"class": "GSAITargetAcceleration",
"language": "GDScript",
"path": "res://steering_ai_framework/GSAITargetAcceleration.gd"
}, {
"base": "Reference",
"class": "GSAIUtils",
"language": "GDScript",
"path": "res://steering_ai_framework/GSAIUtils.gd"
}, {
"base": "ItemTemplate",
"class": "ItemTemplateGD",
"language": "GDScript",
@ -156,6 +301,35 @@ _global_script_class_icons={
"EntityAIGD": "",
"EntityDataGD": "",
"GDCubicVoxelMesher": "",
"GSAIAgentLocation": "",
"GSAIArrive": "",
"GSAIAvoidCollisions": "",
"GSAIBlend": "",
"GSAICohesion": "",
"GSAIEvade": "",
"GSAIFace": "",
"GSAIFlee": "",
"GSAIFollowPath": "",
"GSAIGroupBehavior": "",
"GSAIInfiniteProximity": "",
"GSAIKinematicBody2DAgent": "",
"GSAIKinematicBody3DAgent": "",
"GSAILookWhereYouGo": "",
"GSAIMatchOrientation": "",
"GSAIPath": "",
"GSAIPriority": "",
"GSAIProximity": "",
"GSAIPursue": "",
"GSAIRadiusProximity": "",
"GSAIRigidBody2DAgent": "",
"GSAIRigidBody3DAgent": "",
"GSAISeek": "",
"GSAISeparation": "",
"GSAISpecializedAgent": "",
"GSAISteeringAgent": "",
"GSAISteeringBehavior": "",
"GSAITargetAcceleration": "",
"GSAIUtils": "",
"ItemTemplateGD": "",
"LayeredTextureMaker": "",
"Main": "",

View File

@ -0,0 +1,120 @@
# A specialized steering agent that updates itself every frame so the user does
# not have to using a KinematicBody2D
extends GSAISpecializedAgent
class_name GSAIKinematicBody2DAgent
# SLIDE uses `move_and_slide`
# COLLIDE uses `move_and_collide`
# POSITION changes the `global_position` directly
enum MovementType { SLIDE, COLLIDE, POSITION }
# The KinematicBody2D to keep track of
var body: KinematicBody2D setget _set_body
# The type of movement the body executes
var movement_type: int
var _last_position: Vector2
func _init(_body: KinematicBody2D, _movement_type: int = MovementType.SLIDE) -> void:
if not _body.is_inside_tree():
yield(_body, "ready")
self.body = _body
self.movement_type = _movement_type
# warning-ignore:return_value_discarded
body.get_tree().connect("physics_frame", self, "_on_SceneTree_physics_frame")
# Moves the agent's `body` by target `acceleration`.
# tags: virtual
func _apply_steering(acceleration: GSAITargetAcceleration, delta: float) -> void:
_applied_steering = true
match movement_type:
MovementType.COLLIDE:
_apply_collide_steering(acceleration.linear, delta)
MovementType.SLIDE:
_apply_sliding_steering(acceleration.linear)
_:
_apply_position_steering(acceleration.linear, delta)
_apply_orientation_steering(acceleration.angular, delta)
func _apply_sliding_steering(accel: Vector3) -> void:
var velocity := GSAIUtils.to_vector2(linear_velocity + accel).clamped(linear_speed_max)
if apply_linear_drag:
velocity = velocity.linear_interpolate(Vector2.ZERO, linear_drag_percentage)
velocity = body.move_and_slide(velocity)
if calculate_velocities:
linear_velocity = GSAIUtils.to_vector3(velocity)
func _apply_collide_steering(accel: Vector3, delta: float) -> void:
var velocity := GSAIUtils.clampedv3(linear_velocity + accel, linear_speed_max)
if apply_linear_drag:
velocity = velocity.linear_interpolate(Vector3.ZERO, linear_drag_percentage)
# warning-ignore:return_value_discarded
body.move_and_collide(GSAIUtils.to_vector2(velocity) * delta)
if calculate_velocities:
linear_velocity = velocity
func _apply_position_steering(accel: Vector3, delta: float) -> void:
var velocity := GSAIUtils.clampedv3(linear_velocity + accel, linear_speed_max)
if apply_linear_drag:
velocity = velocity.linear_interpolate(Vector3.ZERO, linear_drag_percentage)
body.global_position += GSAIUtils.to_vector2(velocity) * delta
if calculate_velocities:
linear_velocity = velocity
func _apply_orientation_steering(angular_acceleration: float, delta: float) -> void:
var velocity = angular_velocity + angular_acceleration
if apply_angular_drag:
velocity = lerp(velocity, 0, angular_drag_percentage)
body.rotation += velocity * delta
if calculate_velocities:
angular_velocity = velocity
func _set_body(value: KinematicBody2D) -> void:
body = value
_last_position = body.global_position
_last_orientation = body.rotation
position = GSAIUtils.to_vector3(_last_position)
orientation = _last_orientation
func _on_SceneTree_physics_frame() -> void:
var current_position := body.global_position
var current_orientation := body.rotation
position = GSAIUtils.to_vector3(current_position)
orientation = current_orientation
if calculate_velocities:
if _applied_steering:
_applied_steering = false
else:
linear_velocity = GSAIUtils.clampedv3(
GSAIUtils.to_vector3(_last_position - current_position), linear_speed_max
)
if apply_linear_drag:
linear_velocity = linear_velocity.linear_interpolate(
Vector3.ZERO, linear_drag_percentage
)
angular_velocity = clamp(
_last_orientation - current_orientation, -angular_speed_max, angular_speed_max
)
if apply_angular_drag:
angular_velocity = lerp(angular_velocity, 0, angular_drag_percentage)
_last_position = current_position
_last_orientation = current_orientation

View File

@ -0,0 +1,120 @@
# A specialized steering agent that updates itself every frame so the user does
# not have to using a KinematicBody
extends GSAISpecializedAgent
class_name GSAIKinematicBody3DAgent
# SLIDE uses `move_and_slide`
# COLLIDE uses `move_and_collide`
# POSITION changes the global_position directly
enum MovementType { SLIDE, COLLIDE, POSITION }
# The KinematicBody to keep track of
var body: KinematicBody setget _set_body
# The type of movement the body executes
var movement_type: int
var _last_position: Vector3
func _init(_body: KinematicBody, _movement_type: int = MovementType.SLIDE) -> void:
if not _body.is_inside_tree():
yield(_body, "ready")
self.body = _body
self.movement_type = _movement_type
# warning-ignore:return_value_discarded
self.body.get_tree().connect("physics_frame", self, "_on_SceneTree_physics_frame")
# Moves the agent's `body` by target `acceleration`.
# tags: virtual
func _apply_steering(acceleration: GSAITargetAcceleration, delta: float) -> void:
_applied_steering = true
match movement_type:
MovementType.COLLIDE:
_apply_collide_steering(acceleration.linear, delta)
MovementType.SLIDE:
_apply_sliding_steering(acceleration.linear)
_:
_apply_position_steering(acceleration.linear, delta)
_apply_orientation_steering(acceleration.angular, delta)
func _apply_sliding_steering(accel: Vector3) -> void:
var velocity := GSAIUtils.clampedv3(linear_velocity + accel, linear_speed_max)
if apply_linear_drag:
velocity = velocity.linear_interpolate(Vector3.ZERO, linear_drag_percentage)
velocity = body.move_and_slide(velocity)
if calculate_velocities:
linear_velocity = velocity
func _apply_collide_steering(accel: Vector3, delta: float) -> void:
var velocity := GSAIUtils.clampedv3(linear_velocity + accel, linear_speed_max)
if apply_linear_drag:
velocity = velocity.linear_interpolate(Vector3.ZERO, linear_drag_percentage)
# warning-ignore:return_value_discarded
body.move_and_collide(velocity * delta)
if calculate_velocities:
linear_velocity = velocity
func _apply_position_steering(accel: Vector3, delta: float) -> void:
var velocity := GSAIUtils.clampedv3(linear_velocity + accel, linear_speed_max)
if apply_linear_drag:
velocity = velocity.linear_interpolate(Vector3.ZERO, linear_drag_percentage)
body.global_position += velocity * delta
if calculate_velocities:
linear_velocity = velocity
func _apply_orientation_steering(angular_acceleration: float, delta: float) -> void:
var velocity = angular_velocity + angular_acceleration
if apply_angular_drag:
velocity = lerp(velocity, 0, angular_drag_percentage)
body.rotation.y += velocity * delta
if calculate_velocities:
angular_velocity = velocity
func _set_body(value: KinematicBody) -> void:
body = value
_last_position = body.transform.origin
_last_orientation = body.rotation.y
position = _last_position
orientation = _last_orientation
func _on_SceneTree_physics_frame() -> void:
var current_position := body.transform.origin
var current_orientation := body.rotation.y
position = current_position
orientation = current_orientation
if calculate_velocities:
if _applied_steering:
_applied_steering = false
else:
linear_velocity = GSAIUtils.clampedv3(
_last_position - current_position, linear_speed_max
)
if apply_linear_drag:
linear_velocity = linear_velocity.linear_interpolate(
Vector3.ZERO, linear_drag_percentage
)
angular_velocity = clamp(
_last_orientation - current_orientation, -angular_speed_max, angular_speed_max
)
if apply_angular_drag:
angular_velocity = lerp(angular_velocity, 0, angular_drag_percentage)
_last_position = current_position
_last_orientation = current_orientation

View File

@ -0,0 +1,58 @@
# A specialized steering agent that updates itself every frame so the user does
# not have to using a RigidBody2D
extends GSAISpecializedAgent
class_name GSAIRigidBody2DAgent
# The RigidBody2D to keep track of
var body: RigidBody2D setget _set_body
var _last_position: Vector2
func _init(_body: RigidBody2D) -> void:
if not _body.is_inside_tree():
yield(_body, "ready")
self.body = _body
# Moves the agent's `body` by target `acceleration`.
# tags: virtual
func _apply_steering(acceleration: GSAITargetAcceleration, _delta: float) -> void:
_applied_steering = true
body.apply_central_impulse(GSAIUtils.to_vector2(acceleration.linear))
body.apply_torque_impulse(acceleration.angular)
if calculate_velocities:
linear_velocity = GSAIUtils.to_vector3(body.linear_velocity)
angular_velocity = body.angular_velocity
func _set_body(value: RigidBody2D) -> void:
body = value
_last_position = body.global_position
_last_orientation = body.rotation
position = GSAIUtils.to_vector3(_last_position)
orientation = _last_orientation
func _on_body_ready() -> void:
# warning-ignore:return_value_discarded
body.get_tree().connect("physics_frame", self, "_on_SceneTree_frame")
_set_body(body)
func _on_SceneTree_frame() -> void:
var current_position := body.global_position
var current_orientation := body.rotation
position = GSAIUtils.to_vector3(current_position)
orientation = current_orientation
if calculate_velocities:
if _applied_steering:
_applied_steering = false
else:
linear_velocity = GSAIUtils.to_vector3(body.linear_velocity)
angular_velocity = body.angular_velocity

View File

@ -0,0 +1,60 @@
# A specialized steering agent that updates itself every frame so the user does
# not have to using a RigidBody
extends GSAISpecializedAgent
class_name GSAIRigidBody3DAgent
# The RigidBody to keep track of
var body: RigidBody setget _set_body
var _last_position: Vector3
func _init(_body: RigidBody) -> void:
if not _body.is_inside_tree():
yield(_body, "ready")
self.body = _body
# warning-ignore:return_value_discarded
self.body.get_tree().connect("physics_frame", self, "_on_SceneTree_frame")
# Moves the agent's `body` by target `acceleration`.
# tags: virtual
func _apply_steering(acceleration: GSAITargetAcceleration, _delta: float) -> void:
_applied_steering = true
body.apply_central_impulse(acceleration.linear)
body.apply_torque_impulse(Vector3.UP * acceleration.angular)
if calculate_velocities:
linear_velocity = body.linear_velocity
angular_velocity = body.angular_velocity.y
func _set_body(value: RigidBody) -> void:
body = value
_last_position = body.transform.origin
_last_orientation = body.rotation.y
position = _last_position
orientation = _last_orientation
func _on_body_ready() -> void:
# warning-ignore:return_value_discarded
body.get_tree().connect("physics_frame", self, "_on_SceneTree_frame")
_set_body(body)
func _on_SceneTree_frame() -> void:
var current_position := body.transform.origin
var current_orientation := body.rotation.y
position = current_position
orientation = current_orientation
if calculate_velocities:
if _applied_steering:
_applied_steering = false
else:
linear_velocity = body.linear_velocity
angular_velocity = body.angular_velocity.y

View File

@ -0,0 +1,39 @@
# A base class for a specialized steering agent that updates itself every frame
# so the user does not have to. All other specialized agents derive from this.
# tags: abstract
extends GSAISteeringAgent
class_name GSAISpecializedAgent
# If `true`, calculates linear and angular velocities based on the previous
# frame. When `false`, the user must keep those values updated.
var calculate_velocities := true
# If `true`, interpolates the current linear velocity towards 0 by the
# `linear_drag_percentage` value.
# Does not apply to `RigidBody` and `RigidBody2D` nodes.
var apply_linear_drag := true
# If `true`, interpolates the current angular velocity towards 0 by the
# `angular_drag_percentage` value.
# Does not apply to `RigidBody` and `RigidBody2D` nodes.
var apply_angular_drag := true
# The percentage between the current linear velocity and 0 to interpolate by if
# `apply_linear_drag` is true.
# Does not apply to `RigidBody` and `RigidBody2D` nodes.
var linear_drag_percentage := 0.0
# The percentage between the current angular velocity and 0 to interpolate by if
# `apply_angular_drag` is true.
# Does not apply to `RigidBody` and `RigidBody2D` nodes.
var angular_drag_percentage := 0.0
var _last_orientation: float
var _body_type: int
var _applied_steering := false
# Moves the agent's body by target `acceleration`.
# tags: virtual
func _apply_steering(_acceleration: GSAITargetAcceleration, _delta: float) -> void:
pass

View File

@ -0,0 +1,42 @@
# Calculates acceleration to take an agent to its target's location. The
# calculation attempts to arrive with zero remaining velocity.
class_name GSAIArrive
extends GSAISteeringBehavior
# Target agent to arrive to.
var target: GSAIAgentLocation
# Distance from the target for the agent to be considered successfully
# arrived.
var arrival_tolerance: float
# Distance from the target for the agent to begin slowing down.
var deceleration_radius: float
# Represents the time it takes to change acceleration.
var time_to_reach := 0.1
func _init(agent: GSAISteeringAgent, _target: GSAIAgentLocation).(agent) -> void:
self.target = _target
func _arrive(acceleration: GSAITargetAcceleration, target_position: Vector3) -> void:
var to_target := target_position - agent.position
var distance := to_target.length()
if distance <= arrival_tolerance:
acceleration.set_zero()
else:
var desired_speed := agent.linear_speed_max
if distance <= deceleration_radius:
desired_speed *= distance / deceleration_radius
var desired_velocity := to_target * desired_speed / distance
desired_velocity = ((desired_velocity - agent.linear_velocity) * 1.0 / time_to_reach)
acceleration.linear = GSAIUtils.clampedv3(desired_velocity, agent.linear_acceleration_max)
acceleration.angular = 0
func _calculate_steering(acceleration: GSAITargetAcceleration) -> void:
_arrive(acceleration, target.position)

View File

@ -0,0 +1,74 @@
# Steers the agent to avoid obstacles in its path. Approximates obstacles as
# spheres.
class_name GSAIAvoidCollisions
extends GSAIGroupBehavior
var _first_neighbor: GSAISteeringAgent
var _shortest_time: float
var _first_minimum_separation: float
var _first_distance: float
var _first_relative_position: Vector3
var _first_relative_velocity: Vector3
func _init(agent: GSAISteeringAgent, proximity: GSAIProximity).(agent, proximity) -> void:
pass
func _calculate_steering(acceleration: GSAITargetAcceleration) -> void:
_shortest_time = INF
_first_neighbor = null
_first_minimum_separation = 0
_first_distance = 0
var neighbor_count := proximity._find_neighbors(_callback)
if neighbor_count == 0 or not _first_neighbor:
acceleration.set_zero()
else:
if (
_first_minimum_separation <= 0
or _first_distance < agent.bounding_radius + _first_neighbor.bounding_radius
):
acceleration.linear = _first_neighbor.position - agent.position
else:
acceleration.linear = (
_first_relative_position
+ (_first_relative_velocity * _shortest_time)
)
acceleration.linear = (acceleration.linear.normalized() * -agent.linear_acceleration_max)
acceleration.angular = 0
# Callback for the proximity to call when finding neighbors. Keeps track of every `neighbor`
# that was found but only keeps the one the owning agent will most likely collide with.
# tags: virtual
func _report_neighbor(neighbor: GSAISteeringAgent) -> bool:
var relative_position := neighbor.position - agent.position
var relative_velocity := neighbor.linear_velocity - agent.linear_velocity
var relative_speed_squared := relative_velocity.length_squared()
if relative_speed_squared == 0:
return false
else:
var time_to_collision = -relative_position.dot(relative_velocity) / relative_speed_squared
if time_to_collision <= 0 or time_to_collision >= _shortest_time:
return false
else:
var distance = relative_position.length()
var minimum_separation: float = (
distance
- sqrt(relative_speed_squared) * time_to_collision
)
if minimum_separation > agent.bounding_radius + neighbor.bounding_radius:
return false
else:
_shortest_time = time_to_collision
_first_neighbor = neighbor
_first_minimum_separation = minimum_separation
_first_distance = distance
_first_relative_position = relative_position
_first_relative_velocity = relative_velocity
return true

View File

@ -0,0 +1,47 @@
# Blends multiple steering behaviors into one, and returns a weighted
# acceleration from their calculations.
#
# Stores the behaviors internally as dictionaries of the form
# {
# behavior : GSAISteeringBehavior,
# weight : float
# }
class_name GSAIBlend
extends GSAISteeringBehavior
var _behaviors := []
var _accel := GSAITargetAcceleration.new()
func _init(agent: GSAISteeringAgent).(agent) -> void:
pass
# Appends a behavior to the internal array along with its `weight`.
func add(behavior: GSAISteeringBehavior, weight: float) -> void:
behavior.agent = agent
_behaviors.append({behavior = behavior, weight = weight})
# Returns the behavior at the specified `index`, or an empty `Dictionary` if
# none was found.
func get_behavior_at(index: int) -> Dictionary:
if _behaviors.size() > index:
return _behaviors[index]
printerr("Tried to get index " + str(index) + " in array of size " + str(_behaviors.size()))
return {}
func _calculate_steering(blended_accel: GSAITargetAcceleration) -> void:
blended_accel.set_zero()
for i in range(_behaviors.size()):
var bw: Dictionary = _behaviors[i]
bw.behavior.calculate_steering(_accel)
blended_accel.add_scaled_accel(_accel, bw.weight)
blended_accel.linear = GSAIUtils.clampedv3(blended_accel.linear, agent.linear_acceleration_max)
blended_accel.angular = clamp(
blended_accel.angular, -agent.angular_acceleration_max, agent.angular_acceleration_max
)

View File

@ -0,0 +1,30 @@
# Calculates an acceleration that attempts to move the agent towards the center
# of mass of the agents in the area defined by the `GSAIProximity`.
class_name GSAICohesion
extends GSAIGroupBehavior
var _center_of_mass: Vector3
func _init(agent: GSAISteeringAgent, proximity: GSAIProximity).(agent, proximity) -> void:
pass
func _calculate_steering(acceleration: GSAITargetAcceleration) -> void:
acceleration.set_zero()
_center_of_mass = Vector3.ZERO
var neighbor_count = proximity._find_neighbors(_callback)
if neighbor_count > 0:
_center_of_mass *= 1.0 / neighbor_count
acceleration.linear = (
(_center_of_mass - agent.position).normalized()
* agent.linear_acceleration_max
)
# Callback for the proximity to call when finding neighbors. Adds `neighbor`'s position
# to the center of mass of the group.
# tags: virtual
func _report_neighbor(neighbor: GSAISteeringAgent) -> bool:
_center_of_mass += neighbor.position
return true

View File

@ -0,0 +1,14 @@
# Calculates acceleration to take an agent away from where a target agent is
# moving.
class_name GSAIEvade
extends GSAIPursue
func _init(agent: GSAISteeringAgent, target: GSAISteeringAgent, predict_time_max := 1.0).(
agent, target, predict_time_max
):
pass
func _get_modified_acceleration() -> float:
return -agent.linear_acceleration_max

View File

@ -0,0 +1,29 @@
# Calculates angular acceleration to rotate a target to face its target's
# position. The behavior attemps to arrive with zero remaining angular velocity.
class_name GSAIFace
extends GSAIMatchOrientation
func _init(agent: GSAISteeringAgent, target: GSAIAgentLocation, use_z := false).(
agent, target, use_z
) -> void:
pass
func _face(acceleration: GSAITargetAcceleration, target_position: Vector3) -> void:
var to_target := target_position - agent.position
var distance_squared := to_target.length_squared()
if distance_squared < agent.zero_linear_speed_threshold:
acceleration.set_zero()
else:
var orientation = (
GSAIUtils.vector3_to_angle(to_target)
if use_z
else GSAIUtils.vector2_to_angle(GSAIUtils.to_vector2(to_target))
)
_match_orientation(acceleration, orientation)
func _calculate_steering(acceleration: GSAITargetAcceleration) -> void:
_face(acceleration, target.position)

View File

@ -0,0 +1,15 @@
# Calculates acceleration to take an agent directly away from a target agent.
class_name GSAIFlee
extends GSAISeek
func _init(agent: GSAISteeringAgent, target: GSAIAgentLocation).(agent, target) -> void:
pass
func _calculate_steering(acceleration: GSAITargetAcceleration) -> void:
acceleration.linear = (
(agent.position - target.position).normalized()
* agent.linear_acceleration_max
)
acceleration.angular = 0

View File

@ -0,0 +1,53 @@
# Produces a linear acceleration that moves the agent along the specified path.
class_name GSAIFollowPath
extends GSAIArrive
# The path to follow and travel along.
var path: GSAIPath
# The distance along the path to generate the next target position.
var path_offset := 0.0
# Whether to use `GSAIArrive` behavior on an open path.
var is_arrive_enabled := true
# The amount of time in the future to predict the owning agent's position along
# the path. Setting it to 0.0 will force non-predictive path following.
var prediction_time := 0.0
func _init(agent: GSAISteeringAgent, _path: GSAIPath, _path_offset := 0.0, _prediction_time := 0.0).(
agent, null
) -> void:
self.path = _path
self.path_offset = _path_offset
self.prediction_time = _prediction_time
func _calculate_steering(acceleration: GSAITargetAcceleration) -> void:
var location := (
agent.position
if prediction_time == 0
else agent.position + (agent.linear_velocity * prediction_time)
)
var distance := path.calculate_distance(location)
var target_distance := distance + path_offset
if prediction_time > 0 and path.is_open:
if target_distance < path.calculate_distance(agent.position):
target_distance = path.length
var target_position := path.calculate_target_position(target_distance)
if is_arrive_enabled and path.is_open:
if path_offset >= 0:
if target_distance > path.length - deceleration_radius:
_arrive(acceleration, target_position)
return
else:
if target_distance < deceleration_radius:
_arrive(acceleration, target_position)
return
acceleration.linear = (target_position - agent.position).normalized()
acceleration.linear *= agent.linear_acceleration_max
acceleration.angular = 0

View File

@ -0,0 +1,20 @@
# Calculates an angular acceleration to match an agent's orientation to its
# direction of travel.
class_name GSAILookWhereYouGo
extends GSAIMatchOrientation
func _init(agent: GSAISteeringAgent, use_z := false).(agent, null, use_z) -> void:
pass
func _calculate_steering(accel: GSAITargetAcceleration) -> void:
if agent.linear_velocity.length_squared() < agent.zero_linear_speed_threshold:
accel.set_zero()
else:
var orientation := (
GSAIUtils.vector3_to_angle(agent.linear_velocity)
if use_z
else GSAIUtils.vector2_to_angle(GSAIUtils.to_vector2(agent.linear_velocity))
)
_match_orientation(accel, orientation)

View File

@ -0,0 +1,51 @@
# Calculates an angular acceleration to match an agent's orientation to that of
# its target. Attempts to make the agent arrive with zero remaining angular
# velocity.
class_name GSAIMatchOrientation
extends GSAISteeringBehavior
# The target orientation for the behavior to try and match rotations to.
var target: GSAIAgentLocation
# The amount of distance in radians for the behavior to consider itself close
# enough to be matching the target agent's rotation.
var alignment_tolerance: float
# The amount of distance in radians from the goal to start slowing down.
var deceleration_radius: float
# The amount of time to reach the target velocity
var time_to_reach: float = 0.1
# Whether to use the X and Z components instead of X and Y components when
# determining angles. X and Z should be used in 3D.
var use_z: bool
func _init(agent: GSAISteeringAgent, _target: GSAIAgentLocation, _use_z := false).(agent) -> void:
self.use_z = _use_z
self.target = _target
func _match_orientation(acceleration: GSAITargetAcceleration, desired_orientation: float) -> void:
var rotation := wrapf(desired_orientation - agent.orientation, -PI, PI)
var rotation_size := abs(rotation)
if rotation_size <= alignment_tolerance:
acceleration.set_zero()
else:
var desired_rotation := agent.angular_speed_max
if rotation_size <= deceleration_radius:
desired_rotation *= rotation_size / deceleration_radius
desired_rotation *= rotation / rotation_size
acceleration.angular = ((desired_rotation - agent.angular_velocity) / time_to_reach)
var limited_acceleration := abs(acceleration.angular)
if limited_acceleration > agent.angular_acceleration_max:
acceleration.angular *= (agent.angular_acceleration_max / limited_acceleration)
acceleration.linear = Vector3.ZERO
func _calculate_steering(acceleration: GSAITargetAcceleration) -> void:
_match_orientation(acceleration, target.orientation)

View File

@ -0,0 +1,49 @@
# Container for multiple behaviors that returns the result of the first child
# behavior with non-zero acceleration.
class_name GSAIPriority
extends GSAISteeringBehavior
var _behaviors := []
# The index of the last behavior the container prioritized.
var last_selected_index: int
# If a behavior's acceleration is lower than this threshold, the container
# considers it has an acceleration of zero.
var zero_threshold: float
func _init(agent: GSAISteeringAgent, _zero_threshold := 0.001).(agent) -> void:
self.zero_threshold = _zero_threshold
# Appends a steering behavior as a child of this container.
func add(behavior: GSAISteeringBehavior) -> void:
_behaviors.append(behavior)
# Returns the behavior at the position in the pool referred to by `index`, or
# `null` if no behavior was found.
func get_behavior_at(index: int) -> GSAISteeringBehavior:
if _behaviors.size() > index:
return _behaviors[index]
printerr("Tried to get index " + str(index) + " in array of size " + str(_behaviors.size()))
return null
func _calculate_steering(accel: GSAITargetAcceleration) -> void:
var threshold_squared := zero_threshold * zero_threshold
last_selected_index = -1
var size := _behaviors.size()
if size > 0:
for i in range(size):
last_selected_index = i
var behavior: GSAISteeringBehavior = _behaviors[i]
behavior.calculate_steering(accel)
if accel.get_magnitude_squared() > threshold_squared:
break
else:
accel.set_zero()

View File

@ -0,0 +1,37 @@
# Calculates an acceleration to make an agent intercept another based on the
# target agent's movement.
class_name GSAIPursue
extends GSAISteeringBehavior
# The target agent that the behavior is trying to intercept.
var target: GSAISteeringAgent
# The maximum amount of time in the future the behavior predicts the target's
# location.
var predict_time_max: float
func _init(agent: GSAISteeringAgent, _target: GSAISteeringAgent, _predict_time_max := 1.0).(agent) -> void:
self.target = _target
self.predict_time_max = _predict_time_max
func _calculate_steering(acceleration: GSAITargetAcceleration) -> void:
var target_position := target.position
var distance_squared := (target_position - agent.position).length_squared()
var speed_squared := agent.linear_velocity.length_squared()
var predict_time := predict_time_max
if speed_squared > 0:
var predict_time_squared := distance_squared / speed_squared
if predict_time_squared < predict_time_max * predict_time_max:
predict_time = sqrt(predict_time_squared)
acceleration.linear = ((target_position + (target.linear_velocity * predict_time)) - agent.position).normalized()
acceleration.linear *= _get_modified_acceleration()
acceleration.angular = 0
func _get_modified_acceleration() -> float:
return agent.linear_acceleration_max

View File

@ -0,0 +1,19 @@
# Calculates an acceleration to take an agent to a target agent's position
# directly.
class_name GSAISeek
extends GSAISteeringBehavior
# The target that the behavior aims to move the agent to.
var target: GSAIAgentLocation
func _init(agent: GSAISteeringAgent, _target: GSAIAgentLocation).(agent) -> void:
self.target = _target
func _calculate_steering(acceleration: GSAITargetAcceleration) -> void:
acceleration.linear = (
(target.position - agent.position).normalized()
* agent.linear_acceleration_max
)
acceleration.angular = 0

View File

@ -0,0 +1,42 @@
# Calculates an acceleration that repels the agent from its neighbors in the
# given `GSAIProximity`.
#
# The acceleration is an average based on all neighbors, multiplied by a
# strength decreasing by the inverse square law in relation to distance, and it
# accumulates.
class_name GSAISeparation
extends GSAIGroupBehavior
# The coefficient to calculate how fast the separation strength decays with distance.
var decay_coefficient := 1.0
var _acceleration: GSAITargetAcceleration
func _init(agent: GSAISteeringAgent, proximity: GSAIProximity).(agent, proximity) -> void:
pass
func _calculate_steering(acceleration: GSAITargetAcceleration) -> void:
acceleration.set_zero()
self._acceleration = acceleration
# warning-ignore:return_value_discarded
proximity._find_neighbors(_callback)
# Callback for the proximity to call when finding neighbors. Determines the amount of
# acceleration that `neighbor` imposes based on its distance from the owner agent.
# tags: virtual
func _report_neighbor(neighbor: GSAISteeringAgent) -> bool:
var to_agent := agent.position - neighbor.position
var distance_squared := to_agent.length_squared()
var acceleration_max := agent.linear_acceleration_max
var strength := decay_coefficient / distance_squared
if strength > acceleration_max:
strength = acceleration_max
_acceleration.linear += to_agent * (strength / sqrt(distance_squared))
return true

View File

@ -0,0 +1,80 @@
# Changelog #
This document lists new features, improvements, changes, and bug fixes in every release of the add-on.
## Godot Steering AI Framework 2.1.0 ##
### Features ###
- There is now an `Arrive3d` demo to showcase 3D movement.
### Improvements ###
- All the demos got a bit of attention to improve their feel.
### Changes ###
- `GSAIUtils.vector3_to_angle` now uses the vector's X and Z components to determine angle. Use `GSAIUtils.vector2_to_angle` for 2D use cases.
- `GSAIMatchOrientation` and its subclasses like `GSAIFace` and `GSAILookWhereYouGo` now include a `use_z` property. It should be `true` when using 3D so that facing will be done with the X and Z components.
- The README now mentions a simple way to install the framework.
- Exposed `agent_count` inside the `AvoidCollisionsDemo`.
### Bug fixes ###
- Fixed `GSAIKinematicBody3DAgent` and `GSAIRigidBody3DAgent` trying to use `global_position` instead of `transform.origin`.
- The `SeekFleeDemo`'s boundaries will now match the size of the screen.
- Fixed error when double clicking an item in the DemoPicker.
- Fixed the background sometimes not covering the entire viewport in demos.
## Godot Steering AI Framework 2.0.0 ##
This release brings one new feature and bug fix, and breaking changes to the framework as we renamed all the classes.
**Important**: we renamed all classes from GST\* to GSAI\* (Godot Steering AI). When you upgrade the framework in your project, use the project search and replace feature in Godot (<kbd>Ctrl</kbd> <kbd>Shift</kbd> <kbd>F</kbd>) to find and replace `GST` with `GSAI`.
If you were using `GSTKinematicBodyAgent` or `GSTRigidBodyAgent`, search and replace them respectively with `GSAIKinematicBody3DAgent` and `GSAIRigidBody3DAgent`.
We decided to make this change as soon as possible, as the framework was released a few days ago.
### Features ###
- There is now a main scene with a demo picker, so you can select and play any demo on the fly.
- The demo projects now support resizing and toggling fullscreen with <kbd>F11</kbd>.
### Improvements ###
- We handled all warnings in the framework, so using it won't add warnings to your projects.
### Changes ###
- Renamed all classes from `GST*` (Godot Steering Toolkit) to `GSAI*` (Godot Steering AI).
- Removed `GSTNode2DAgent`, `GSTNodeAgent`, and `GSTSpatialAgent` classes.
- For specialized steering agents, `GSAIKinematicBody2DAgent`, `GSAIRigidBody2DAgent`, or their 3D equivalent.
- If you intend to write your own movement system instead of using Godot's, the base class `GSTSpecializedAgent` is there to help you.
- Renamed `GSAIRigidBodyAgent` and `GSAIRigidBodyAgent` to `GSAIRigidBody3DAgent` and `GSAIRigidBody3DAgent` respectively.
- 3D nodes like `Sprite`, `KinematicBody`, etc. are being renamed to `Sprite3D`, `KinematicBody3D`, etc. in the upcoming Godot 4.0 release, to be consistent with 2D nodes. We decided to rename them now instead of breaking compatibility in a future release.
### Bug fixes ###
- GSTFollowPath no longer loops back around itself on open paths when `predict_time` is non-zero.
## Godot Steering AI Framework 1.0.0 ##
This is the first major release of the framework. It comes with:
- All the essential steering behaviors: `Arrive`, `AvoidCollisions`, `Blend`, `Cohesion`, `Evade`, `Face`, `Flee`, `FollowPath`, `LookWhereYouGo`, `MatchOrientation`, `Priority`, `Pursue`, `Seek`, `Separation`.
- Group behaviors and detecting neighbors.
- Blending and prioritized behaviors.
- Specialized types to code agents based on physics bodies:
- For 2D games, `KinematicBody2DAgent` and `RigidBody2DAgent`.
- For 3D games, `KinematicBody3DAgent` and `RigidBody3DAgent`.
- 9 Godot demos to learn straight from the code.
### Manual ###
To get started, check out the framework's [manual](https://www.gdquest.com/docs/godot-steering-toolkit/).
There, you can also find the full [code reference](https://www.gdquest.com/docs/godot-steering-toolkit/reference/).
*Note*: we generate the code reference from docstrings in the source code with [GDScript Docs Maker](https://github.com/GDQuest/gdscript-docs-maker).

View File

@ -0,0 +1,7 @@
# Represents an agent with only a location and an orientation.
class_name GSAIAgentLocation
# The agent's position in space.
var position := Vector3.ZERO
# The agent's orientation on its Y axis rotation.
var orientation := 0.0

View File

@ -0,0 +1,19 @@
# Base type for group-based steering behaviors.
class_name GSAIGroupBehavior
extends GSAISteeringBehavior
# Container to find neighbors of the agent and calculate group behavior.
var proximity: GSAIProximity
var _callback := funcref(self, "_report_neighbor")
func _init(agent: GSAISteeringAgent, _proximity: GSAIProximity).(agent) -> void:
self.proximity = _proximity
# Internal callback for the behavior to define whether or not a member is
# relevant
# tags: virtual
func _report_neighbor(_neighbor: GSAISteeringAgent) -> bool:
return false

View File

@ -0,0 +1,132 @@
# Represents a path made up of Vector3 waypoints, split into segments path
# follow behaviors can use.
class_name GSAIPath
extends Reference
# If `false`, the path loops.
var is_open: bool
# Total length of the path.
var length: float
var _segments: Array
var _nearest_point_on_segment: Vector3
var _nearest_point_on_path: Vector3
func _init(waypoints: Array, _is_open := false) -> void:
self.is_open = _is_open
create_path(waypoints)
_nearest_point_on_segment = waypoints[0]
_nearest_point_on_path = waypoints[0]
# Creates a path from a list of waypoints.
func create_path(waypoints: Array) -> void:
if not waypoints or waypoints.size() < 2:
printerr("Waypoints cannot be null and must contain at least two (2) waypoints.")
return
_segments = []
length = 0
var current: Vector3 = waypoints.front()
var previous: Vector3
for i in range(1, waypoints.size(), 1):
previous = current
if i < waypoints.size():
current = waypoints[i]
elif is_open:
break
else:
current = waypoints[0]
var segment := GSAISegment.new(previous, current)
length += segment.length
segment.cumulative_length = length
_segments.append(segment)
# Returns the distance from `agent_current_position` to the next waypoint.
func calculate_distance(agent_current_position: Vector3) -> float:
if _segments.size() == 0:
return 0.0
var smallest_distance_squared: float = INF
var nearest_segment: GSAISegment
for i in range(_segments.size()):
var segment: GSAISegment = _segments[i]
var distance_squared := _calculate_point_segment_distance_squared(
segment.begin, segment.end, agent_current_position
)
if distance_squared < smallest_distance_squared:
_nearest_point_on_path = _nearest_point_on_segment
smallest_distance_squared = distance_squared
nearest_segment = segment
var length_on_path := (
nearest_segment.cumulative_length
- _nearest_point_on_path.distance_to(nearest_segment.end)
)
return length_on_path
# Calculates a target position from the path's starting point based on the `target_distance`.
func calculate_target_position(target_distance: float) -> Vector3:
if is_open:
target_distance = clamp(target_distance, 0, length)
else:
if target_distance < 0:
target_distance = length + fmod(target_distance, length)
elif target_distance > length:
target_distance = fmod(target_distance, length)
var desired_segment: GSAISegment
for i in range(_segments.size()):
var segment: GSAISegment = _segments[i]
if segment.cumulative_length >= target_distance:
desired_segment = segment
break
if not desired_segment:
desired_segment = _segments.back()
var distance := desired_segment.cumulative_length - target_distance
return (
((desired_segment.begin - desired_segment.end) * (distance / desired_segment.length))
+ desired_segment.end
)
# Returns the position of the first point on the path.
func get_start_point() -> Vector3:
return _segments.front().begin
# Returns the position of the last point on the path.
func get_end_point() -> Vector3:
return _segments.back().end
func _calculate_point_segment_distance_squared(start: Vector3, end: Vector3, position: Vector3) -> float:
_nearest_point_on_segment = start
var start_end := end - start
var start_end_length_squared := start_end.length_squared()
if start_end_length_squared != 0:
var t = (position - start).dot(start_end) / start_end_length_squared
_nearest_point_on_segment += start_end * clamp(t, 0, 1)
return _nearest_point_on_segment.distance_squared_to(position)
class GSAISegment:
var begin: Vector3
var end: Vector3
var length: float
var cumulative_length: float
func _init(_begin: Vector3, _end: Vector3) -> void:
self.begin = _begin
self.end = _end
length = _begin.distance_to(_end)

View File

@ -0,0 +1,27 @@
# Adds velocity, speed, and size data to `GSAIAgentLocation`.
#
# It is the character's responsibility to keep this information up to date for
# the steering toolkit to work correctly.
extends GSAIAgentLocation
class_name GSAISteeringAgent
# The amount of velocity to be considered effectively not moving.
var zero_linear_speed_threshold := 0.01
# The maximum speed at which the agent can move.
var linear_speed_max := 0.0
# The maximum amount of acceleration that any behavior can apply to the agent.
var linear_acceleration_max := 0.0
# The maximum amount of angular speed at which the agent can rotate.
var angular_speed_max := 0.0
# The maximum amount of angular acceleration that any behavior can apply to an
# agent.
var angular_acceleration_max := 0.0
# Current velocity of the agent.
var linear_velocity := Vector3.ZERO
# Current angular velocity of the agent.
var angular_velocity := 0.0
# The radius of the sphere that approximates the agent's size in space.
var bounding_radius := 0.0
# Used internally by group behaviors and proximities to mark the agent as already
# considered.
var is_tagged := false

View File

@ -0,0 +1,29 @@
# Base class for all steering behaviors.
#
# Steering behaviors calculate the linear and the angular acceleration to be
# to the agent that owns them.
#
# The `calculate_steering` function is the entry point for all behaviors.
# Individual steering behaviors encapsulate the steering logic.
class_name GSAISteeringBehavior
# If `false`, all calculations return zero amounts of acceleration.
var is_enabled := true
# The AI agent on which the steering behavior bases its calculations.
var agent: GSAISteeringAgent
func _init(_agent: GSAISteeringAgent) -> void:
self.agent = _agent
# Sets the `acceleration` with the behavior's desired amount of acceleration.
func calculate_steering(acceleration: GSAITargetAcceleration) -> void:
if is_enabled:
_calculate_steering(acceleration)
else:
acceleration.set_zero()
func _calculate_steering(acceleration: GSAITargetAcceleration) -> void:
acceleration.set_zero()

View File

@ -0,0 +1,32 @@
# A desired linear and angular amount of acceleration requested by the steering
# system.
class_name GSAITargetAcceleration
# Linear acceleration
var linear := Vector3.ZERO
# Angular acceleration
var angular := 0.0
# Sets the linear and angular components to 0.
func set_zero() -> void:
linear.x = 0.0
linear.y = 0.0
linear.z = 0.0
angular = 0.0
# Adds `accel`'s components, multiplied by `scalar`, to this one.
func add_scaled_accel(accel: GSAITargetAcceleration, scalar: float) -> void:
linear += accel.linear * scalar
angular += accel.angular * scalar
# Returns the squared magnitude of the linear and angular components.
func get_magnitude_squared() -> float:
return linear.length_squared() + angular * angular
# Returns the magnitude of the linear and angular components.
func get_magnitude() -> float:
return sqrt(get_magnitude_squared())

View File

@ -0,0 +1,36 @@
# Math and vector utility functions.
class_name GSAIUtils
# Returns the `vector` with its length capped to `limit`.
static func clampedv3(vector: Vector3, limit: float) -> Vector3:
var length_squared := vector.length_squared()
var limit_squared := limit * limit
if length_squared > limit_squared:
vector *= sqrt(limit_squared / length_squared)
return vector
# Returns an angle in radians between the positive X axis and the `vector`.
#
# This assumes orientation for 3D agents that are upright and rotate
# around the Y axis.
static func vector3_to_angle(vector: Vector3) -> float:
return atan2(vector.x, vector.z)
# Returns an angle in radians between the positive X axis and the `vector`.
static func vector2_to_angle(vector: Vector2) -> float:
return atan2(vector.x, -vector.y)
# Returns a directional vector from the given orientation angle.
#
# This assumes orientation for 2D agents or 3D agents that are upright and
# rotate around the Y axis.
static func angle_to_vector2(angle: float) -> Vector2:
return Vector2(sin(-angle), cos(angle))
# Returns a vector2 with `vector`'s x and y components.
static func to_vector2(vector: Vector3) -> Vector2:
return Vector2(vector.x, vector.y)
# Returns a vector3 with `vector`'s x and y components and 0 in z.
static func to_vector3(vector: Vector2) -> Vector3:
return Vector3(vector.x, vector.y, 0)

View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2020 GDQuest
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -0,0 +1,26 @@
# Determines any agent that is in the specified list as being neighbors with the
# owner agent, regardless of distance.
extends GSAIProximity
class_name GSAIInfiniteProximity
func _init(agent: GSAISteeringAgent, agents: Array).(agent, agents) -> void:
pass
# Returns a number of neighbors based on a `callback` function.
#
# `_find_neighbors` calls `callback` for each agent in the `agents` array and
# adds one to the count if its `callback` returns true.
# tags: virtual
func _find_neighbors(callback: FuncRef) -> int:
var neighbor_count := 0
var agent_count := agents.size()
for i in range(agent_count):
var current_agent := agents[i] as GSAISteeringAgent
if current_agent != agent:
if callback.call_func(current_agent):
neighbor_count += 1
return neighbor_count

View File

@ -0,0 +1,22 @@
# Base container type that stores data to find the neighbors of an agent.
extends Reference
class_name GSAIProximity
# The owning agent whose neighbors are found in the group
var agent: GSAISteeringAgent
# The agents who are part of this group and could be potential neighbors
var agents := []
func _init(_agent: GSAISteeringAgent, _agents: Array) -> void:
self.agent = _agent
self.agents = _agents
# Returns a number of neighbors based on a `callback` function.
#
# `_find_neighbors` calls `callback` for each agent in the `agents` array and
# adds one to the count if its `callback` returns true.
# tags: virtual
func _find_neighbors(_callback: FuncRef) -> int:
return 0

View File

@ -0,0 +1,56 @@
# Determines any agent that is in the specified list as being neighbors with the owner agent if
# they lie within the specified radius.
extends GSAIProximity
class_name GSAIRadiusProximity
# The radius around the owning agent to find neighbors in
var radius := 0.0
var _last_frame := 0
var _scene_tree: SceneTree
func _init(agent: GSAISteeringAgent, agents: Array, _radius: float).(agent, agents) -> void:
self.radius = _radius
_scene_tree = Engine.get_main_loop()
# Returns a number of neighbors based on a `callback` function.
#
# `_find_neighbors` calls `callback` for each agent in the `agents` array that lie within
# the radius around the owning agent and adds one to the count if its `callback` returns true.
# tags: virtual
func _find_neighbors(callback: FuncRef) -> int:
var agent_count := agents.size()
var neighbor_count := 0
var current_frame := _scene_tree.get_frame() if _scene_tree else -_last_frame
if current_frame != _last_frame:
_last_frame = current_frame
var owner_position := agent.position
for i in range(agent_count):
var current_agent := agents[i] as GSAISteeringAgent
if current_agent != agent:
var distance_squared := owner_position.distance_squared_to(current_agent.position)
var range_to := radius + current_agent.bounding_radius
if distance_squared < range_to * range_to:
if callback.call_func(current_agent):
current_agent.is_tagged = true
neighbor_count += 1
continue
current_agent.is_tagged = false
else:
for i in range(agent_count):
var current_agent = agents[i] as GSAISteeringAgent
if current_agent != agent and current_agent.is_tagged:
if callback.call_func(current_agent):
neighbor_count += 1
return neighbor_count

View File

@ -0,0 +1,69 @@
# Godot Steering AI Framework #
![Project banner](./assets/banner.svg)
This project is a framework to code complex and smooth AI movement in GDScript, using steering behaviors. It works in both 2D and 3D games.
It supports all essential steering behaviors like flee, follow, look at, but also blended behaviors, group behaviors, avoiding neighbors, following a path, following the leader, and much more.
<!-- markdown-toc start - Don't edit this section. Run M-x markdown-toc-refresh-toc -->
**Table of Contents**
- [Introduction](#introduction)
- [The framework](#the-framework)
- [Installation](#installation)
- [Getting Started](#getting-started)
- [More information and resources](#more-information-and-resources)
- [Example usage](#example-usage)
<!-- markdown-toc end -->
## Introduction ##
In the 1990s, [Craig Reynolds](http://www.red3d.com/cwr/) developed algorithms for common AI behaviors. They allowed AI agents to seek out or flee from a target, follow a pre-defined path, or face in a particular direction. They were simple, repeatable tasks that could be broken down into programming algorithms, which made them easy to reuse, maintain, combine, and extend.
While an AI agent's next action is based on decision making and planning algorithms, steering behaviors dictate how it will move from one frame to the next. They use available information and calculate where to move at that moment.
Joining these systems together can give sophisticated and graceful movement while also being more efficient than complex pathfinding algorithms like A\*.
## The framework ##
This project is a framework for the [Godot game engine](https://godotengine.org/). It takes inspiration from the excellent [GDX-AI](https://github.com/libgdx/gdx-ai) framework for the [LibGDX](https://libgdx.badlogicgames.com/) java-based framework.
Every class in the framework extends Godot's [Reference](https://docs.godotengine.org/en/latest/classes/class_reference.html) type. There is no need to have a complex scene tree; you can contain that has to do with the AI's movement inside GDScript classes.
### How it works ###
In GSAI, a steering agent represents a character or a vehicle. The agent stores its position, orientation, maximum speeds, and current velocity. The agent stores a steering behavior that calculates a linear or angular change in velocity based on its information.
The coder then applies that acceleration in whatever ways is appropriate to the character to change its velocities, like RigidBody's `apply_impulse`, or a KinematicBody's `move_and_slide`.
## Documentation ##
The framework's documentation and code reference are both available on the [GDQuest](https://www.gdquest.com/docs/godot-steering-ai-framework/getting-started) website.
Here are some guides to get you started:
1. [How to install the framework](https://www.gdquest.com/docs/godot-steering-ai-framework/how-to-install/)
1. [Getting Started](https://www.gdquest.com/docs/godot-steering-ai-framework/getting-started/)
1. [Code reference](https://www.gdquest.com/docs/godot-steering-ai-framework/reference/)
## Contributing ##
If you encounter a bug or you have an idea to improve the tool, please [open an issue](https://github.com/GDQuest/gdscript-docs-maker/issues/new).
If you want to contribute to the project, for instance by fixing a bug or adding a feature, check out our:
1. [Contributor's guidelines](https://www.gdquest.com/docs/guidelines/contributing-to/gdquest-projects/).
1. [GDScript style guide](https://www.gdquest.com/docs/guidelines/best-practices/godot-gdscript/)
## Support us ##
Our work on Free Software is sponsored by our [Godot game creation courses](https://gdquest.mavenseed.com/). Consider getting one to support us!
*If you like our work, please star the repository! This helps more people find it.*
## Join the community ##
- You can join the GDQuest community and come chat with us on [Discord](https://discord.gg/CHYVgar)
- For quick news, follow us on [Twitter](https://twitter.com/nathangdquest)
- We release video tutorials and major updates on [YouTube](https://youtube.com/c/gdquest)