diff --git a/steering_ai/Demos/Arrive/ArriveDemo.gd b/steering_ai/Demos/Arrive/ArriveDemo.gd
new file mode 100644
index 0000000..c18049b
--- /dev/null
+++ b/steering_ai/Demos/Arrive/ArriveDemo.gd
@@ -0,0 +1,51 @@
+extends Node
+
+export (float, 0, 3200, 100) var linear_speed_max := 800.0 setget set_linear_speed_max
+export (float, 0, 10000, 100) var linear_acceleration_max := 80.0 setget set_linear_acceleration_max
+export (float, 0, 100, 0.1) var arrival_tolerance := 25.0 setget set_arrival_tolerance
+export (float, 0, 500, 10) var deceleration_radius := 125.0 setget set_deceleration_radius
+
+onready var arriver := $Arriver
+onready var target_drawer := $TargetDrawer
+
+
+func _ready() -> void:
+ arriver.setup(linear_speed_max, linear_acceleration_max, arrival_tolerance, deceleration_radius)
+
+
+func _unhandled_input(event: InputEvent) -> void:
+ if event is InputEventMouseButton and event.button_index == BUTTON_LEFT and event.is_pressed():
+ arriver.target.position = Vector3(event.position.x, event.position.y, 0)
+ target_drawer.update()
+
+
+func set_arrival_tolerance(value: float) -> void:
+ arrival_tolerance = value
+ if not is_inside_tree():
+ return
+
+ arriver.arrive.arrival_tolerance = value
+
+
+func set_deceleration_radius(value: float) -> void:
+ deceleration_radius = value
+ if not is_inside_tree():
+ return
+
+ arriver.arrive.deceleration_radius = value
+
+
+func set_linear_speed_max(value: float) -> void:
+ linear_speed_max = value
+ if not is_inside_tree():
+ return
+
+ arriver.agent.linear_speed_max = value
+
+
+func set_linear_acceleration_max(value: float) -> void:
+ linear_acceleration_max = value
+ if not is_inside_tree():
+ return
+
+ arriver.agent.linear_acceleration_max = value
diff --git a/steering_ai/Demos/Arrive/ArriveDemo.tscn b/steering_ai/Demos/Arrive/ArriveDemo.tscn
new file mode 100644
index 0000000..8b02e8e
--- /dev/null
+++ b/steering_ai/Demos/Arrive/ArriveDemo.tscn
@@ -0,0 +1,39 @@
+[gd_scene load_steps=8 format=2]
+
+[ext_resource path="res://Demos/Arrive/Arriver.gd" type="Script" id=1]
+[ext_resource path="res://Demos/Utils/DemoInterface.tscn" type="PackedScene" id=2]
+[ext_resource path="res://Demos/Arrive/ArriveDemo.gd" type="Script" id=3]
+[ext_resource path="res://Demos/Utils/BackgroudLayer.tscn" type="PackedScene" id=4]
+[ext_resource path="res://Demos/Arrive/TargetDrawer.gd" type="Script" id=5]
+[ext_resource path="res://Demos/Utils/CircleDraw.gd" type="Script" id=6]
+
+[sub_resource type="CircleShape2D" id=1]
+radius = 23.2163
+
+[node name="ArriveDemo" type="Node"]
+script = ExtResource( 3 )
+linear_speed_max = 1600.0
+linear_acceleration_max = 5000.0
+arrival_tolerance = 35.0
+deceleration_radius = 180.0
+
+[node name="BackgroudLayer" parent="." instance=ExtResource( 4 )]
+
+[node name="TargetDrawer" type="Node2D" parent="."]
+script = ExtResource( 5 )
+
+[node name="Arriver" type="KinematicBody2D" parent="."]
+show_behind_parent = true
+position = Vector2( 960, 540 )
+script = ExtResource( 1 )
+
+[node name="CollisionShape2D" type="CollisionShape2D" parent="Arriver"]
+shape = SubResource( 1 )
+script = ExtResource( 6 )
+inner_color = Color( 0.235294, 0.639216, 0.439216, 1 )
+outer_color = Color( 0.560784, 0.870588, 0.364706, 1 )
+stroke = 6.0
+
+[node name="DemoInterface" parent="." instance=ExtResource( 2 )]
+text_bbcode = "Arrive Demo
+Mouse click to make the [color=lime]green \"Player\"[/color] move to the [color=fuchsia]purple target[/color]"
diff --git a/steering_ai/Demos/Arrive/Arriver.gd b/steering_ai/Demos/Arrive/Arriver.gd
new file mode 100644
index 0000000..fb83b57
--- /dev/null
+++ b/steering_ai/Demos/Arrive/Arriver.gd
@@ -0,0 +1,32 @@
+extends KinematicBody2D
+
+var agent := GSAIKinematicBody2DAgent.new()
+var target := GSAIAgentLocation.new()
+var arrive := GSAIArrive.new()
+var _accel := GSAITargetAcceleration.new()
+
+var _velocity := Vector2()
+var _drag := 0.1
+
+func _init() -> void:
+ agent.body = self
+ arrive.agent = agent
+ arrive.target = target
+
+func _physics_process(delta: float) -> void:
+ arrive.calculate_steering(_accel)
+ agent.apply_steering(_accel, delta)
+
+
+func setup(
+ linear_speed_max: float,
+ linear_acceleration_max: float,
+ arrival_tolerance: float,
+ deceleration_radius: float
+) -> void:
+ agent.linear_speed_max = linear_speed_max
+ agent.linear_acceleration_max = linear_acceleration_max
+ agent.linear_drag_percentage = _drag
+ arrive.deceleration_radius = deceleration_radius
+ arrive.arrival_tolerance = arrival_tolerance
+ target.position = agent.position
diff --git a/steering_ai/Demos/Arrive/TargetDrawer.gd b/steering_ai/Demos/Arrive/TargetDrawer.gd
new file mode 100644
index 0000000..a5dccf7
--- /dev/null
+++ b/steering_ai/Demos/Arrive/TargetDrawer.gd
@@ -0,0 +1,17 @@
+extends Node2D
+
+var deceleration_radius_color : Color = Color(1.0, 0.419, 0.592, 0.5)
+var arrival_tolerance_color : Color = Color(0.278, 0.231, 0.47, 0.3)
+
+var arriver: Node2D
+
+
+func _ready() -> void:
+ yield(owner, "ready")
+ arriver = owner.arriver
+
+
+func _draw():
+ var target_position := GSAIUtils.to_vector2(arriver.target.position)
+ draw_circle(target_position, owner.deceleration_radius, deceleration_radius_color)
+ draw_circle(target_position, owner.arrival_tolerance, arrival_tolerance_color)
diff --git a/steering_ai/Demos/Arrive3d/Arrive3dDemo.tscn b/steering_ai/Demos/Arrive3d/Arrive3dDemo.tscn
new file mode 100644
index 0000000..8ffcc48
--- /dev/null
+++ b/steering_ai/Demos/Arrive3d/Arrive3dDemo.tscn
@@ -0,0 +1,96 @@
+[gd_scene load_steps=14 format=2]
+
+[ext_resource path="res://Demos/Utils/DemoInterface.tscn" type="PackedScene" id=1]
+[ext_resource path="res://Demos/Arrive3d/Camera.gd" type="Script" id=2]
+[ext_resource path="res://Demos/Arrive3d/Seek3dDemo.gd" type="Script" id=3]
+[ext_resource path="res://Demos/Arrive3d/Seeker.gd" type="Script" id=4]
+[ext_resource path="res://Demos/Arrive3d/SeekerMat.tres" type="Material" id=5]
+
+[sub_resource type="CapsuleShape" id=1]
+
+[sub_resource type="CapsuleMesh" id=2]
+
+[sub_resource type="CubeMesh" id=3]
+material = ExtResource( 5 )
+size = Vector3( 0.5, 0.5, 1 )
+
+[sub_resource type="CylinderMesh" id=4]
+top_radius = 2.0
+bottom_radius = 2.0
+height = 0.1
+
+[sub_resource type="SpatialMaterial" id=5]
+albedo_color = Color( 0.945098, 0.85098, 0.0745098, 1 )
+
+[sub_resource type="BoxShape" id=6]
+extents = Vector3( 1000, 0.1, 1000 )
+
+[sub_resource type="PlaneMesh" id=7]
+size = Vector2( 250, 250 )
+
+[sub_resource type="SpatialMaterial" id=8]
+albedo_color = Color( 0.0941176, 0.235294, 0.486275, 1 )
+
+[node name="Arrive3dDemo" type="Node"]
+script = ExtResource( 3 )
+linear_speed_max = 50.0
+linear_acceleration_max = 53.2
+deceleration_radius = 10.8
+angular_speed_max = 550
+angular_accel_max = 910
+
+[node name="Arriver" type="KinematicBody" parent="."]
+script = ExtResource( 4 )
+
+[node name="CollisionShape" type="CollisionShape" parent="Arriver"]
+transform = Transform( 1, 0, 0, 0, -1.62921e-07, 1, 0, -1, -1.62921e-07, 0, 1.5, 0 )
+shape = SubResource( 1 )
+
+[node name="Capsule" type="MeshInstance" parent="Arriver"]
+transform = Transform( 1, 0, 0, 0, -1.62921e-07, 1, 0, -1, -1.62921e-07, 0, 1.5, 0 )
+mesh = SubResource( 2 )
+material/0 = ExtResource( 5 )
+
+[node name="Nose" type="MeshInstance" parent="Arriver"]
+transform = Transform( 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 2, 1.25 )
+mesh = SubResource( 3 )
+material/0 = null
+
+[node name="Camera" type="Camera" parent="."]
+transform = Transform( 0.989952, 0.0720094, -0.121693, 0.0339305, 0.714503, 0.69881, 0.137271, -0.695917, 0.70488, -7.68317, 14.1265, 25.616 )
+current = true
+script = ExtResource( 2 )
+
+[node name="RayCast" type="RayCast" parent="Camera"]
+enabled = true
+cast_to = Vector3( -627, 200, -777 )
+collision_mask = 2
+
+[node name="MouseTarget" type="Spatial" parent="."]
+transform = Transform( 1, 0, 7.45058e-09, 0, 1, 0, 7.45058e-09, 0, 1, -4.76837e-07, 9.53674e-07, 1.90735e-06 )
+
+[node name="MeshInstance" type="MeshInstance" parent="MouseTarget"]
+mesh = SubResource( 4 )
+material/0 = SubResource( 5 )
+
+[node name="StaticBody" type="StaticBody" parent="."]
+transform = Transform( 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -0.1, 0 )
+collision_layer = 2
+collision_mask = 2
+
+[node name="CollisionShape" type="CollisionShape" parent="StaticBody"]
+shape = SubResource( 6 )
+
+[node name="Ground" type="MeshInstance" parent="."]
+mesh = SubResource( 7 )
+material/0 = SubResource( 8 )
+
+[node name="DirectionalLight" type="DirectionalLight" parent="."]
+transform = Transform( -0.588165, 0.462179, -0.663666, -0.804031, -0.245728, 0.541436, 0.087159, 0.852061, 0.516134, -17.6076, 12.1748, 0 )
+light_energy = 0.5
+shadow_enabled = true
+
+[node name="DemoInterface" parent="." instance=ExtResource( 1 )]
+mouse_filter = 2
+text_bbcode = "3D Arrive Demo
+Move the mouse about the field to have the agent turn towards and smoothly arrive at the target marker."
diff --git a/steering_ai/Demos/Arrive3d/Camera.gd b/steering_ai/Demos/Arrive3d/Camera.gd
new file mode 100644
index 0000000..4970295
--- /dev/null
+++ b/steering_ai/Demos/Arrive3d/Camera.gd
@@ -0,0 +1,24 @@
+extends Camera
+
+var target: Spatial
+
+onready var ray := $RayCast
+
+
+func _unhandled_input(event: InputEvent) -> void:
+ if event is InputEventMouseMotion:
+ _set_target_position(event.position)
+
+
+func setup(_target: Spatial) -> void:
+ self.target = _target
+ _set_target_position(get_viewport().get_mouse_position())
+
+
+func _set_target_position(position: Vector2) -> void:
+ var to = project_local_ray_normal(position) * 10000
+ ray.cast_to = to
+ ray.force_raycast_update()
+ if ray.is_colliding():
+ var point = ray.get_collision_point()
+ target.transform.origin = point
diff --git a/steering_ai/Demos/Arrive3d/Seek3dDemo.gd b/steering_ai/Demos/Arrive3d/Seek3dDemo.gd
new file mode 100644
index 0000000..151ed7b
--- /dev/null
+++ b/steering_ai/Demos/Arrive3d/Seek3dDemo.gd
@@ -0,0 +1,92 @@
+extends Node
+
+export (float, 0, 100, 5) var linear_speed_max := 10.0 setget set_linear_speed_max
+export (float, 0, 100, 0.1) var linear_acceleration_max := 1.0 setget set_linear_acceleration_max
+export (float, 0, 50, 0.1) var arrival_tolerance := 0.5 setget set_arrival_tolerance
+export (float, 0, 50, 0.1) var deceleration_radius := 5.0 setget set_deceleration_radius
+export (int, 0, 1080, 10) var angular_speed_max := 270 setget set_angular_speed_max
+export (int, 0, 2048, 10) var angular_accel_max := 45 setget set_angular_accel_max
+export (int, 0, 178, 2) var align_tolerance := 5 setget set_align_tolerance
+export (int, 0, 180, 2) var angular_deceleration_radius := 45 setget set_angular_deceleration_radius
+
+onready var target := $MouseTarget
+onready var arriver := $Arriver
+
+
+func _ready() -> void:
+ arriver.setup(
+ deg2rad(align_tolerance),
+ deg2rad(angular_deceleration_radius),
+ deg2rad(angular_accel_max),
+ deg2rad(angular_speed_max),
+ deceleration_radius,
+ arrival_tolerance,
+ linear_acceleration_max,
+ linear_speed_max,
+ target
+ )
+ $Camera.setup(target)
+
+
+func set_align_tolerance(value: int) -> void:
+ align_tolerance = value
+ if not is_inside_tree():
+ return
+
+ arriver.face.alignment_tolerance = deg2rad(value)
+
+
+func set_angular_deceleration_radius(value: int) -> void:
+ deceleration_radius = value
+ if not is_inside_tree():
+ return
+
+ arriver.face.deceleration_radius = deg2rad(value)
+
+
+func set_angular_accel_max(value: int) -> void:
+ angular_accel_max = value
+ if not is_inside_tree():
+ return
+
+ arriver.agent.angular_acceleration_max = deg2rad(value)
+
+
+func set_angular_speed_max(value: int) -> void:
+ angular_speed_max = value
+ if not is_inside_tree():
+ return
+
+ arriver.agent.angular_speed_max = deg2rad(value)
+
+
+func set_arrival_tolerance(value: float) -> void:
+ arrival_tolerance = value
+ if not is_inside_tree():
+ return
+
+ arriver.arrive.arrival_tolerance = value
+
+
+func set_deceleration_radius(value: float) -> void:
+ deceleration_radius = value
+ if not is_inside_tree():
+ return
+
+ arriver.arrive.deceleration_radius = value
+
+
+func set_linear_speed_max(value: float) -> void:
+ linear_speed_max = value
+ if not is_inside_tree():
+ return
+
+ arriver.agent.linear_speed_max = value
+
+
+func set_linear_acceleration_max(value: float) -> void:
+ linear_acceleration_max = value
+ if not is_inside_tree():
+ return
+
+ arriver.agent.linear_acceleration_max = value
diff --git a/steering_ai/Demos/Arrive3d/Seeker.gd b/steering_ai/Demos/Arrive3d/Seeker.gd
new file mode 100644
index 0000000..4d3d5bf
--- /dev/null
+++ b/steering_ai/Demos/Arrive3d/Seeker.gd
@@ -0,0 +1,64 @@
+extends KinematicBody
+
+var target_node: Spatial
+
+var agent : GSAIKinematicBody3DAgent = null
+var target : GSAIAgentLocation = null
+var accel : GSAITargetAcceleration = null
+var blend : GSAIBlend = null
+var face : GSAIFace = null
+var arrive : GSAIArrive = null
+
+func _init() -> void:
+ agent = GSAIKinematicBody3DAgent.new()
+ agent.body = self
+
+ target = GSAIAgentLocation.new()
+ accel = GSAITargetAcceleration.new()
+ blend = GSAIBlend.new()
+ blend.agent = agent
+
+ face = GSAIFace.new()
+ face.agent = agent
+ face.target = target
+ face.use_z = true
+
+ arrive = GSAIArrive.new()
+ arrive.agent = agent
+ arrive.target = target
+
+func _physics_process(delta: float) -> void:
+ target.position = target_node.transform.origin
+ target.position.y = transform.origin.y
+ blend.calculate_steering(accel)
+ agent.apply_steering(accel, delta)
+
+
+func setup(
+ align_tolerance: float,
+ angular_deceleration_radius: float,
+ angular_accel_max: float,
+ angular_speed_max: float,
+ deceleration_radius: float,
+ arrival_tolerance: float,
+ linear_acceleration_max: float,
+ linear_speed_max: float,
+ _target: Spatial
+) -> void:
+ agent.linear_speed_max = linear_speed_max
+ agent.linear_acceleration_max = linear_acceleration_max
+ agent.linear_drag_percentage = 0.05
+ agent.angular_acceleration_max = angular_accel_max
+ agent.angular_speed_max = angular_speed_max
+ agent.angular_drag_percentage = 0.1
+
+ arrive.arrival_tolerance = arrival_tolerance
+ arrive.deceleration_radius = deceleration_radius
+
+ face.alignment_tolerance = align_tolerance
+ face.deceleration_radius = angular_deceleration_radius
+
+ target_node = _target
+ self.target.position = target_node.transform.origin
+ blend.add_behavior(arrive, 1)
+ blend.add_behavior(face, 1)
diff --git a/steering_ai/Demos/Arrive3d/SeekerMat.tres b/steering_ai/Demos/Arrive3d/SeekerMat.tres
new file mode 100644
index 0000000..8a9f1f9
--- /dev/null
+++ b/steering_ai/Demos/Arrive3d/SeekerMat.tres
@@ -0,0 +1,4 @@
+[gd_resource type="SpatialMaterial" format=2]
+
+[resource]
+albedo_color = Color( 0.152941, 0.764706, 0.247059, 1 )
diff --git a/steering_ai/Demos/AvoidCollisions/AvoidCollisionsDemo.gd b/steering_ai/Demos/AvoidCollisions/AvoidCollisionsDemo.gd
new file mode 100644
index 0000000..f82b2a1
--- /dev/null
+++ b/steering_ai/Demos/AvoidCollisions/AvoidCollisionsDemo.gd
@@ -0,0 +1,40 @@
+extends Node
+
+export (float, 0, 1000, 40) var linear_speed_max := 350.0 setget set_linear_speed_max
+export (float, 0, 4000, 2) var linear_acceleration_max := 40.0 setget set_linear_accel_max
+export (float, 0, 500, 10) var proximity_radius := 140.0 setget set_proximity_radius
+export var draw_proximity := true setget set_draw_proximity
+
+onready var spawner := $Spawner
+
+
+func set_linear_speed_max(value: float) -> void:
+ linear_speed_max = value
+ if not is_inside_tree():
+ return
+
+ spawner.set_linear_speed_max(value)
+
+
+func set_linear_accel_max(value: float) -> void:
+ linear_acceleration_max = value
+ if not is_inside_tree():
+ return
+
+ spawner.set_linear_accel_max(value)
+
+
+func set_proximity_radius(value: float) -> void:
+ proximity_radius = value
+ if not is_inside_tree():
+ return
+
+ spawner.set_proximity_radius(value)
+
+
+func set_draw_proximity(value: bool) -> void:
+ draw_proximity = value
+ if not is_inside_tree():
+ return
+
+ spawner.set_draw_proximity(value)
diff --git a/steering_ai/Demos/AvoidCollisions/AvoidCollisionsDemo.tscn b/steering_ai/Demos/AvoidCollisions/AvoidCollisionsDemo.tscn
new file mode 100644
index 0000000..8dfadb0
--- /dev/null
+++ b/steering_ai/Demos/AvoidCollisions/AvoidCollisionsDemo.tscn
@@ -0,0 +1,26 @@
+[gd_scene load_steps=6 format=2]
+
+[ext_resource path="res://Demos/AvoidCollisions/Spawner.gd" type="Script" id=1]
+[ext_resource path="res://Demos/AvoidCollisions/AvoidCollisionsDemo.gd" type="Script" id=2]
+[ext_resource path="res://Demos/AvoidCollisions/Avoider.tscn" type="PackedScene" id=3]
+[ext_resource path="res://Demos/Utils/DemoInterface.tscn" type="PackedScene" id=4]
+[ext_resource path="res://Demos/Utils/BackgroudLayer.tscn" type="PackedScene" id=5]
+
+[node name="AvoidCollisionsDemo" type="Node"]
+script = ExtResource( 2 )
+linear_speed_max = 520.0
+linear_acceleration_max = 2250.0
+proximity_radius = 100.0
+
+[node name="BackgroudLayer" parent="." instance=ExtResource( 5 )]
+
+[node name="Spawner" type="Node2D" parent="."]
+script = ExtResource( 1 )
+avoider_template = ExtResource( 3 )
+inner_color = Color( 0.235294, 0.639216, 0.439216, 1 )
+outer_color = Color( 0.560784, 0.870588, 0.364706, 1 )
+agent_count = 80
+
+[node name="DemoInterface" parent="." instance=ExtResource( 4 )]
+text_bbcode = "Avoid Collisions Demo
+Watch each agent try to keep traveling in a particular direction, but prioritize avoiding collisions with other agents."
diff --git a/steering_ai/Demos/AvoidCollisions/Avoider.gd b/steering_ai/Demos/AvoidCollisions/Avoider.gd
new file mode 100644
index 0000000..80d96c2
--- /dev/null
+++ b/steering_ai/Demos/AvoidCollisions/Avoider.gd
@@ -0,0 +1,112 @@
+extends KinematicBody2D
+
+var draw_proximity: bool
+
+var _boundary_right: float
+var _boundary_bottom: float
+var _radius: float
+var _accel := GSAITargetAcceleration.new()
+var _velocity := Vector2.ZERO
+var _direction := Vector2()
+var _drag := 0.1
+var _color := Color(0.4, 1.0, 0.89, 0.3)
+
+onready var collision := $CollisionShape2D
+var agent :GSAIKinematicBody2DAgent= null
+var proximity :GSAIRadiusProximity= null
+var avoid :GSAIAvoidCollisions= null
+var target :GSAIAgentLocation= null
+var seek :GSAISeek= null
+var priority :GSAIPriority= null
+
+func _ready() -> void:
+ agent = GSAIKinematicBody2DAgent.new()
+ agent.body = self
+
+ proximity = GSAIRadiusProximity.new()
+ proximity.agent = agent
+ proximity.radius = 140
+
+ avoid = GSAIAvoidCollisions.new()
+ avoid.agent = agent
+ avoid.proximity = proximity
+
+ target = GSAIAgentLocation.new()
+
+ seek = GSAISeek.new()
+ seek.agent = agent
+ seek.target = target
+
+ priority = GSAIPriority.new()
+ priority.agent = agent
+ priority.zero_threshold = 0.0001
+
+func _draw() -> void:
+ if draw_proximity:
+ draw_circle(Vector2.ZERO, proximity.radius, _color)
+
+
+func _physics_process(delta: float) -> void:
+ target.position.x = agent.position.x + _direction.x * _radius
+ target.position.y = agent.position.y + _direction.y * _radius
+
+ priority.calculate_steering(_accel)
+ agent.apply_steering(_accel, delta)
+
+
+func setup(
+ linear_speed_max: float,
+ linear_accel_max: float,
+ proximity_radius: float,
+ boundary_right: float,
+ boundary_bottom: float,
+ _draw_proximity: bool,
+ rng: RandomNumberGenerator
+) -> void:
+ rng.randomize()
+ _direction = Vector2(rng.randf_range(-1, 1), rng.randf_range(-1, 1)).normalized()
+
+ agent.linear_speed_max = linear_speed_max
+ agent.linear_acceleration_max = linear_accel_max
+
+ proximity.radius = proximity_radius
+ _boundary_bottom = boundary_bottom
+ _boundary_right = boundary_right
+
+ _radius = collision.shape.radius
+ agent.bounding_radius = _radius
+
+ agent.linear_drag_percentage = _drag
+
+ self.draw_proximity = _draw_proximity
+
+ priority.add_behavior(avoid)
+ priority.add_behavior(seek)
+
+
+func set_proximity_agents(agents: Array) -> void:
+ proximity.agents = agents
+
+
+func set_random_nonoverlapping_position(others: Array, distance_from_boundary_min: float) -> void:
+ var rng := RandomNumberGenerator.new()
+ rng.randomize()
+ var tries_max := max(100, others.size() * others.size())
+ while tries_max > 0:
+ tries_max -= 1
+ global_position.x = rng.randf_range(
+ distance_from_boundary_min, _boundary_right - distance_from_boundary_min
+ )
+ global_position.y = rng.randf_range(
+ distance_from_boundary_min, _boundary_bottom - distance_from_boundary_min
+ )
+ var done := true
+ for i in range(others.size()):
+ var other: Node2D = others[i]
+ if (
+ other.global_position.distance_to(position)
+ <= _radius * 2 + distance_from_boundary_min
+ ):
+ done = false
+ if done:
+ break
diff --git a/steering_ai/Demos/AvoidCollisions/Avoider.tscn b/steering_ai/Demos/AvoidCollisions/Avoider.tscn
new file mode 100644
index 0000000..97f32cc
--- /dev/null
+++ b/steering_ai/Demos/AvoidCollisions/Avoider.tscn
@@ -0,0 +1,17 @@
+[gd_scene load_steps=4 format=2]
+
+[ext_resource path="res://Demos/Utils/CircleDraw.gd" type="Script" id=1]
+[ext_resource path="res://Demos/AvoidCollisions/Avoider.gd" type="Script" id=2]
+
+[sub_resource type="CircleShape2D" id=1]
+radius = 21.3503
+
+[node name="Avoider" type="KinematicBody2D"]
+script = ExtResource( 2 )
+
+[node name="CollisionShape2D" type="CollisionShape2D" parent="."]
+shape = SubResource( 1 )
+script = ExtResource( 1 )
+inner_color = Color( 0.890196, 0.411765, 0.337255, 1 )
+outer_color = Color( 1, 0.709804, 0.439216, 1 )
+stroke = 5.0
diff --git a/steering_ai/Demos/AvoidCollisions/Spawner.gd b/steering_ai/Demos/AvoidCollisions/Spawner.gd
new file mode 100644
index 0000000..fedb269
--- /dev/null
+++ b/steering_ai/Demos/AvoidCollisions/Spawner.gd
@@ -0,0 +1,66 @@
+extends Node2D
+
+export var avoider_template: PackedScene
+export var inner_color := Color()
+export var outer_color := Color()
+export var agent_count := 60
+
+var boundaries: Vector2
+
+
+func _ready() -> void:
+ boundaries = Vector2(
+ ProjectSettings["display/window/size/width"], ProjectSettings["display/window/size/height"]
+ )
+ var rng := RandomNumberGenerator.new()
+ var avoiders := []
+ var avoider_agents := []
+ for i in range(agent_count):
+ var avoider := avoider_template.instance()
+ add_child(avoider)
+ avoider.setup(
+ owner.linear_speed_max,
+ owner.linear_acceleration_max,
+ owner.proximity_radius,
+ boundaries.x,
+ boundaries.y,
+ true if i == 0 and owner.draw_proximity else false,
+ rng
+ )
+ avoider_agents.append(avoider.agent)
+ avoider.set_random_nonoverlapping_position(avoiders, 16)
+ if i == 0:
+ avoider.collision.inner_color = inner_color
+ avoider.collision.outer_color = outer_color
+ avoiders.append(avoider)
+ if i % 10 == 0:
+ yield(get_tree(), "idle_frame")
+ for child in get_children():
+ child.set_proximity_agents(avoider_agents)
+
+
+func _physics_process(_delta: float) -> void:
+ for child in get_children():
+ child.global_position = child.global_position.posmodv(boundaries)
+
+
+func set_linear_speed_max(value: float) -> void:
+ for child in get_children():
+ child.agent.linear_speed_max = value
+
+
+func set_linear_accel_max(value: float) -> void:
+ for child in get_children():
+ child.agent.linear_acceleration_max = value
+
+
+func set_proximity_radius(value: float) -> void:
+ for child in get_children():
+ child.proximity.radius = value
+ get_child(0).update()
+
+
+func set_draw_proximity(value: bool) -> void:
+ var child := get_child(0)
+ child.draw_proximity = value
+ child.update()
diff --git a/steering_ai/Demos/DemoPickerUI.gd b/steering_ai/Demos/DemoPickerUI.gd
new file mode 100644
index 0000000..657d5cc
--- /dev/null
+++ b/steering_ai/Demos/DemoPickerUI.gd
@@ -0,0 +1,28 @@
+class_name DemoPickerUI
+extends Control
+
+# warning-ignore:unused_signal
+signal demo_requested
+
+var demo_path := "" setget set_demo_path
+
+onready var list: ItemList = $VBoxContainer/ItemList
+onready var button: Button = $VBoxContainer/Button
+
+
+func _ready() -> void:
+ # warning-ignore:return_value_discarded
+ list.connect("demo_selected", self, "set_demo_path")
+ # warning-ignore:return_value_discarded
+ list.connect("item_activated", self, "_on_ItemList_item_activated")
+ # warning-ignore:return_value_discarded
+ button.connect("pressed", self, "emit_signal", ["demo_requested"])
+ demo_path = list.file_paths[0]
+
+
+func set_demo_path(value: String) -> void:
+ demo_path = value
+
+
+func _on_ItemList_item_activated(_index: int) -> void:
+ emit_signal("demo_requested")
diff --git a/steering_ai/Demos/DemoPlayer.gd b/steering_ai/Demos/DemoPlayer.gd
new file mode 100644
index 0000000..cfcfa9f
--- /dev/null
+++ b/steering_ai/Demos/DemoPlayer.gd
@@ -0,0 +1,16 @@
+extends Node2D
+
+
+func load_demo(scene_path: String) -> void:
+ if not scene_path:
+ return
+
+ var demo = load(scene_path)
+ if demo:
+ add_child(demo.instance())
+
+
+func unload() -> void:
+ for node in get_children():
+ call_deferred("remove_child", node)
+ node.queue_free()
diff --git a/steering_ai/Demos/DemoSelector.tscn b/steering_ai/Demos/DemoSelector.tscn
new file mode 100644
index 0000000..6d36925
--- /dev/null
+++ b/steering_ai/Demos/DemoSelector.tscn
@@ -0,0 +1,81 @@
+[gd_scene load_steps=7 format=2]
+
+[ext_resource path="res://Demos/PopulateItemList.gd" type="Script" id=1]
+[ext_resource path="res://assets/theme/gdquest.theme" type="Theme" id=2]
+[ext_resource path="res://assets/sprites/background.png" type="Texture" id=3]
+[ext_resource path="res://Demos/DemoPickerUI.gd" type="Script" id=4]
+[ext_resource path="res://Demos/DemoPlayer.gd" type="Script" id=5]
+[ext_resource path="res://Demos/Demos.gd" type="Script" id=6]
+
+[node name="Demos" type="Node"]
+script = ExtResource( 6 )
+
+[node name="DemoPlayer" type="Node2D" parent="."]
+script = ExtResource( 5 )
+
+[node name="DemoPickerUI" type="Control" parent="."]
+anchor_right = 1.0
+anchor_bottom = 1.0
+theme = ExtResource( 2 )
+script = ExtResource( 4 )
+
+[node name="TextureRect" type="TextureRect" parent="DemoPickerUI"]
+anchor_right = 1.0
+anchor_bottom = 1.0
+rect_min_size = Vector2( 1024, 600 )
+size_flags_horizontal = 3
+size_flags_vertical = 3
+texture = ExtResource( 3 )
+expand = true
+stretch_mode = 2
+
+[node name="VBoxContainer" type="VBoxContainer" parent="DemoPickerUI"]
+anchor_left = 0.5
+anchor_top = 0.5
+anchor_right = 0.5
+anchor_bottom = 0.5
+margin_left = -341.0
+margin_top = -290.0
+margin_right = 341.0
+margin_bottom = 290.0
+rect_min_size = Vector2( 682, 0 )
+size_flags_horizontal = 3
+size_flags_vertical = 3
+alignment = 1
+__meta__ = {
+"_edit_use_anchors_": false
+}
+
+[node name="ItemList" type="ItemList" parent="DemoPickerUI/VBoxContainer"]
+margin_top = 231.0
+margin_right = 682.0
+margin_bottom = 240.0
+auto_height = true
+script = ExtResource( 1 )
+
+[node name="Button" type="Button" parent="DemoPickerUI/VBoxContainer"]
+margin_left = 201.0
+margin_top = 248.0
+margin_right = 481.0
+margin_bottom = 348.0
+rect_min_size = Vector2( 280, 100 )
+size_flags_horizontal = 4
+size_flags_vertical = 13
+text = "Load scene"
+
+[node name="ButtonGoBack" type="Button" parent="."]
+visible = false
+anchor_top = 1.0
+anchor_bottom = 1.0
+margin_left = 48.0
+margin_top = -156.0
+margin_right = 328.0
+margin_bottom = -56.0
+rect_min_size = Vector2( 280, 100 )
+size_flags_horizontal = 4
+size_flags_vertical = 13
+theme = ExtResource( 2 )
+text = "Go back"
+__meta__ = {
+"_edit_use_anchors_": false
+}
diff --git a/steering_ai/Demos/Demos.gd b/steering_ai/Demos/Demos.gd
new file mode 100644
index 0000000..e8a9150
--- /dev/null
+++ b/steering_ai/Demos/Demos.gd
@@ -0,0 +1,30 @@
+extends Node
+
+onready var demo_picker: DemoPickerUI = $DemoPickerUI
+onready var demo_player := $DemoPlayer
+onready var button_go_back: Button = $ButtonGoBack
+
+
+func _ready() -> void:
+ # warning-ignore:return_value_discarded
+ demo_picker.connect("demo_requested", self, "_on_DemoPickerUI_demo_requested")
+ # warning-ignore:return_value_discarded
+ button_go_back.connect("pressed", self, "_on_ButtonGoBack_pressed")
+
+
+func _input(event: InputEvent) -> void:
+ if event.is_action_pressed("toggle_fullscreen"):
+ OS.window_fullscreen = not OS.window_fullscreen
+ get_tree().set_input_as_handled()
+
+
+func _on_DemoPickerUI_demo_requested() -> void:
+ demo_player.load_demo(demo_picker.demo_path)
+ demo_picker.hide()
+ button_go_back.show()
+
+
+func _on_ButtonGoBack_pressed() -> void:
+ demo_player.unload()
+ button_go_back.hide()
+ demo_picker.show()
diff --git a/steering_ai/Demos/Face/FaceDemo.gd b/steering_ai/Demos/Face/FaceDemo.gd
new file mode 100644
index 0000000..282d941
--- /dev/null
+++ b/steering_ai/Demos/Face/FaceDemo.gd
@@ -0,0 +1,61 @@
+extends Node
+
+export (int, 0, 1080, 2) var angular_speed_max := 120 setget set_angular_speed_max
+export (int, 0, 2048, 2) var angular_accel_max := 10 setget set_angular_accel_max
+export (int, 0, 180, 2) var align_tolerance := 5 setget set_align_tolerance
+export (int, 0, 359, 2) var deceleration_radius := 45 setget set_deceleration_radius
+export (float, 0, 1000, 40) var player_speed := 600.0 setget set_player_speed
+
+onready var player := $Player
+onready var turret := $Turret
+
+
+func _ready() -> void:
+ player.speed = player_speed
+ turret.setup(
+ player.agent,
+ deg2rad(align_tolerance),
+ deg2rad(deceleration_radius),
+ deg2rad(angular_accel_max),
+ deg2rad(angular_speed_max)
+ )
+
+
+func set_align_tolerance(value: int) -> void:
+ align_tolerance = value
+ if not is_inside_tree():
+ return
+
+ turret.face.alignment_tolerance = deg2rad(value)
+
+
+func set_deceleration_radius(value: int) -> void:
+ deceleration_radius = value
+ if not is_inside_tree():
+ return
+
+ turret.face.deceleration_radius = deg2rad(value)
+
+
+func set_angular_accel_max(value: int) -> void:
+ angular_accel_max = value
+ if not is_inside_tree():
+ return
+
+ turret.agent.angular_acceleration_max = deg2rad(value)
+
+
+func set_angular_speed_max(value: int) -> void:
+ angular_speed_max = value
+ if not is_inside_tree():
+ return
+
+ turret.agent.angular_speed_max = deg2rad(value)
+
+
+func set_player_speed(value: float) -> void:
+ player_speed = value
+ if not is_inside_tree():
+ return
+
+ player.speed = player_speed
diff --git a/steering_ai/Demos/Face/FaceDemo.tscn b/steering_ai/Demos/Face/FaceDemo.tscn
new file mode 100644
index 0000000..8115e44
--- /dev/null
+++ b/steering_ai/Demos/Face/FaceDemo.tscn
@@ -0,0 +1,48 @@
+[gd_scene load_steps=9 format=2]
+
+[ext_resource path="res://Demos/Face/Turret.gd" type="Script" id=1]
+[ext_resource path="res://Demos/Face/FaceDemo.gd" type="Script" id=2]
+[ext_resource path="res://Demos/Face/Player.gd" type="Script" id=3]
+[ext_resource path="res://Demos/Utils/DemoInterface.tscn" type="PackedScene" id=4]
+[ext_resource path="res://Demos/Utils/BackgroudLayer.tscn" type="PackedScene" id=5]
+[ext_resource path="res://Demos/Utils/CircleDraw.gd" type="Script" id=8]
+
+[sub_resource type="CircleShape2D" id=1]
+radius = 20.2633
+
+[sub_resource type="CircleShape2D" id=2]
+radius = 37.1052
+
+[node name="FaceDemo" type="Node"]
+script = ExtResource( 2 )
+angular_speed_max = 662
+angular_accel_max = 924
+deceleration_radius = 136
+
+[node name="BackgroudLayer" parent="." instance=ExtResource( 5 )]
+
+[node name="Player" type="KinematicBody2D" parent="."]
+position = Vector2( 687.363, 351.005 )
+script = ExtResource( 3 )
+
+[node name="CollisionShape2D" type="CollisionShape2D" parent="Player"]
+shape = SubResource( 1 )
+script = ExtResource( 8 )
+inner_color = Color( 0.235294, 0.639216, 0.439216, 1 )
+outer_color = Color( 0.560784, 0.870588, 0.364706, 1 )
+stroke = 6.0
+
+[node name="Turret" type="KinematicBody2D" parent="."]
+position = Vector2( 984.348, 571.959 )
+script = ExtResource( 1 )
+
+[node name="CollisionShape2D" type="CollisionShape2D" parent="Turret"]
+shape = SubResource( 2 )
+script = ExtResource( 8 )
+inner_color = Color( 0.890196, 0.411765, 0.337255, 1 )
+outer_color = Color( 1, 0.709804, 0.439216, 1 )
+stroke = 8.0
+
+[node name="DemoInterface" parent="." instance=ExtResource( 4 )]
+text_bbcode = "Face Demo
+Move the [color=lime]green player[/color] around with WASD and notice the [color=#ffb570]orange turret[/color] orient itself"
diff --git a/steering_ai/Demos/Face/Player.gd b/steering_ai/Demos/Face/Player.gd
new file mode 100644
index 0000000..d37cc17
--- /dev/null
+++ b/steering_ai/Demos/Face/Player.gd
@@ -0,0 +1,18 @@
+extends KinematicBody2D
+
+var speed: float
+
+onready var agent := GSAIAgentLocation.new()
+
+
+func _physics_process(_delta: float) -> void:
+ var movement := _get_movement()
+ move_and_slide(movement * speed)
+ agent.position = Vector3(global_position.x, global_position.y, 0)
+
+
+func _get_movement() -> Vector2:
+ return Vector2(
+ Input.get_action_strength("sf_right") - Input.get_action_strength("sf_left"),
+ Input.get_action_strength("sf_down") - Input.get_action_strength("sf_up")
+ )
diff --git a/steering_ai/Demos/Face/Turret.gd b/steering_ai/Demos/Face/Turret.gd
new file mode 100644
index 0000000..9568909
--- /dev/null
+++ b/steering_ai/Demos/Face/Turret.gd
@@ -0,0 +1,47 @@
+extends KinematicBody2D
+
+var face: GSAIFace
+var agent := GSAIKinematicBody2DAgent.new()
+
+var _accel := GSAITargetAcceleration.new()
+var _angular_drag := 0.1
+var _cannon: Rect2
+var _color: Color
+
+onready var collision_shape := $CollisionShape2D
+
+
+func _ready() -> void:
+ agent.body = self
+
+ var radius = collision_shape.shape.radius
+ _cannon = Rect2(Vector2(-5, 0), Vector2(10, -radius * 2))
+ _color = collision_shape.outer_color
+
+
+func _physics_process(delta: float) -> void:
+ face.calculate_steering(_accel)
+ agent.apply_steering(_accel, delta)
+
+
+func _draw() -> void:
+ draw_rect(_cannon, _color)
+
+
+func setup(
+ player_agent: GSAIAgentLocation,
+ align_tolerance: float,
+ deceleration_radius: float,
+ angular_accel_max: float,
+ angular_speed_max: float
+) -> void:
+ face = GSAIFace.new()
+ face.agent = agent
+ face.target = player_agent
+
+ face.alignment_tolerance = align_tolerance
+ face.deceleration_radius = deceleration_radius
+
+ agent.angular_acceleration_max = angular_accel_max
+ agent.angular_speed_max = angular_speed_max
+ agent.angular_drag_percentage = _angular_drag
diff --git a/steering_ai/Demos/FollowPath/Drawer.gd b/steering_ai/Demos/FollowPath/Drawer.gd
new file mode 100644
index 0000000..fcecf1d
--- /dev/null
+++ b/steering_ai/Demos/FollowPath/Drawer.gd
@@ -0,0 +1,53 @@
+extends Node2D
+
+signal path_established(points)
+
+var active_points := []
+var is_drawing := false
+var distance_threshold := 100.0
+
+
+func _unhandled_input(event: InputEvent) -> void:
+ if event is InputEventMouseMotion:
+ if is_drawing:
+ active_points.append(event.position)
+ update()
+ elif event is InputEventMouseButton:
+ if event.pressed and event.button_index == BUTTON_LEFT:
+ active_points.clear()
+ active_points.append(event.position)
+ is_drawing = true
+ update()
+ elif not event.pressed:
+ is_drawing = false
+ if active_points.size() >= 2:
+ _simplify()
+
+
+func _draw() -> void:
+ if is_drawing:
+ for point in active_points:
+ draw_circle(point, 2, Color.red)
+ else:
+ if active_points.size() > 0:
+ draw_circle(active_points.front(), 2, Color.red)
+ draw_circle(active_points.back(), 2, Color.yellow)
+ draw_polyline(active_points, Color.skyblue, 1.0)
+
+
+func _simplify() -> void:
+ var first: Vector2 = active_points.front()
+ var last: Vector2 = active_points.back()
+ var key := first
+ var simplified_path := [first]
+ for i in range(1, active_points.size()):
+ var point: Vector2 = active_points[i]
+ var distance := point.distance_to(key)
+ if distance > distance_threshold:
+ key = point
+ simplified_path.append(key)
+ active_points = simplified_path
+ if active_points.back() != last:
+ active_points.append(last)
+ update()
+ emit_signal("path_established", active_points)
diff --git a/steering_ai/Demos/FollowPath/FollowPathDemo.gd b/steering_ai/Demos/FollowPath/FollowPathDemo.gd
new file mode 100644
index 0000000..cc26ce3
--- /dev/null
+++ b/steering_ai/Demos/FollowPath/FollowPathDemo.gd
@@ -0,0 +1,70 @@
+extends Node
+
+export (float, 0, 2000, 40) var linear_speed_max := 600.0 setget set_linear_speed_max
+export (float, 0, 9000, 10.0) var linear_acceleration_max := 40.0 setget set_linear_acceleration_max
+export (float, 0, 100, 0.1) var arrival_tolerance := 10.0 setget set_arrival_tolerance
+export (float, 0, 500, 10) var deceleration_radius := 100.0 setget set_deceleration_radius
+export (float, 0, 5, 0.1) var predict_time := 0.3 setget set_predict_time
+export (float, 0, 200, 10.0) var path_offset := 20.0 setget set_path_offset
+
+onready var drawer := $Drawer
+onready var follower := $PathFollower
+
+
+func _ready() -> void:
+ follower.setup(
+ path_offset,
+ predict_time,
+ linear_acceleration_max,
+ linear_speed_max,
+ deceleration_radius,
+ arrival_tolerance
+ )
+
+
+func set_linear_speed_max(value: float) -> void:
+ linear_speed_max = value
+ if not is_inside_tree():
+ return
+
+ follower.agent.linear_speed_max = value
+
+
+func set_linear_acceleration_max(value: float) -> void:
+ linear_acceleration_max = value
+ if not is_inside_tree():
+ return
+
+ follower.agent.linear_acceleration_max = value
+
+
+func set_arrival_tolerance(value: float) -> void:
+ arrival_tolerance = value
+ if not is_inside_tree():
+ return
+
+ follower.follow.arrival_tolerance = value
+
+
+func set_deceleration_radius(value: float) -> void:
+ deceleration_radius = value
+ if not is_inside_tree():
+ return
+
+ follower.follow.deceleration_radius = value
+
+
+func set_predict_time(value: float) -> void:
+ predict_time = value
+ if not is_inside_tree():
+ return
+
+ follower.follow.prediction_time = value
+
+
+func set_path_offset(value: float) -> void:
+ path_offset = value
+ if not is_inside_tree():
+ return
+
+ follower.follow.path_offset = value
diff --git a/steering_ai/Demos/FollowPath/FollowPathDemo.tscn b/steering_ai/Demos/FollowPath/FollowPathDemo.tscn
new file mode 100644
index 0000000..9614780
--- /dev/null
+++ b/steering_ai/Demos/FollowPath/FollowPathDemo.tscn
@@ -0,0 +1,37 @@
+[gd_scene load_steps=8 format=2]
+
+[ext_resource path="res://Demos/FollowPath/Drawer.gd" type="Script" id=1]
+[ext_resource path="res://Demos/Utils/DemoInterface.tscn" type="PackedScene" id=2]
+[ext_resource path="res://Demos/FollowPath/PathFollower.gd" type="Script" id=3]
+[ext_resource path="res://Demos/FollowPath/FollowPathDemo.gd" type="Script" id=4]
+[ext_resource path="res://Demos/Utils/BackgroudLayer.tscn" type="PackedScene" id=5]
+[ext_resource path="res://Demos/Utils/CircleDraw.gd" type="Script" id=6]
+
+[sub_resource type="CircleShape2D" id=1]
+radius = 24.1954
+
+[node name="FollowPathDemo" type="Node"]
+script = ExtResource( 4 )
+linear_speed_max = 920.0
+linear_acceleration_max = 3740.0
+deceleration_radius = 200.0
+
+[node name="BackgroudLayer" parent="." instance=ExtResource( 5 )]
+
+[node name="Drawer" type="Node2D" parent="."]
+script = ExtResource( 1 )
+
+[node name="PathFollower" type="KinematicBody2D" parent="."]
+position = Vector2( 960, 540 )
+script = ExtResource( 3 )
+
+[node name="CollisionShape2D" type="CollisionShape2D" parent="PathFollower"]
+shape = SubResource( 1 )
+script = ExtResource( 6 )
+inner_color = Color( 0.235294, 0.639216, 0.439216, 1 )
+outer_color = Color( 0.560784, 0.870588, 0.364706, 1 )
+stroke = 6.0
+
+[node name="DemoInterface" parent="." instance=ExtResource( 2 )]
+text_bbcode = "Follow Path Demo
+Use the mouse to draw a path on screen and watch the [color=lime]green \"Agent\"[/color] follow it to the end."
diff --git a/steering_ai/Demos/FollowPath/PathFollower.gd b/steering_ai/Demos/FollowPath/PathFollower.gd
new file mode 100644
index 0000000..1c21c39
--- /dev/null
+++ b/steering_ai/Demos/FollowPath/PathFollower.gd
@@ -0,0 +1,58 @@
+extends KinematicBody2D
+
+var _velocity := Vector2.ZERO
+var _accel := GSAITargetAcceleration.new()
+var _valid := false
+var _drag := 0.1
+
+var agent := GSAIKinematicBody2DAgent.new()
+var path := GSAIPath.new()
+var follow := GSAIFollowPath.new()
+
+func _ready() -> void:
+ agent.body = self
+
+ path.initialize(
+ [
+ Vector3(global_position.x, global_position.y, 0),
+ Vector3(global_position.x, global_position.y, 0)
+ ],
+ true
+ )
+
+ follow.agent = agent
+ follow.path = path
+ follow.agent = agent
+
+func setup(
+ path_offset: float,
+ predict_time: float,
+ accel_max: float,
+ speed_max: float,
+ decel_radius: float,
+ arrival_tolerance: float
+) -> void:
+ owner.drawer.connect("path_established", self, "_on_Drawer_path_established")
+ follow.path_offset = path_offset
+ follow.prediction_time = predict_time
+ follow.deceleration_radius = decel_radius
+ follow.arrival_tolerance = arrival_tolerance
+
+ agent.linear_acceleration_max = accel_max
+ agent.linear_speed_max = speed_max
+ agent.linear_drag_percentage = _drag
+
+
+func _physics_process(delta: float) -> void:
+ if _valid:
+ follow.calculate_steering(_accel)
+ agent.apply_steering(_accel, delta)
+
+
+func _on_Drawer_path_established(points: Array) -> void:
+ var positions : PoolVector3Array = PoolVector3Array()
+ for p in points:
+ positions.append(Vector3(p.x, p.y, 0))
+
+ path.create_path(positions)
+ _valid = true
diff --git a/steering_ai/Demos/GroupBehaviors/GroupBehaviorsDemo.gd b/steering_ai/Demos/GroupBehaviors/GroupBehaviorsDemo.gd
new file mode 100644
index 0000000..51ab7ea
--- /dev/null
+++ b/steering_ai/Demos/GroupBehaviors/GroupBehaviorsDemo.gd
@@ -0,0 +1,79 @@
+extends Node
+
+onready var spawner := $Spawner
+
+export (float, 0, 2000, 40.0) var linear_speed_max := 600.0 setget set_linear_speed_max
+export (float, 0, 9000, 2.0) var linear_accel_max := 40.0 setget set_linear_accel_max
+export (float, 0, 300, 2.0) var proximity_radius := 140.0 setget set_proximity_radius
+export (float, 0, 200000, 250) var separation_decay_coefficient := 2000.0 setget set_separation_decay_coef
+export (float, 0, 2, 0.1) var cohesion_strength := 0.1 setget set_cohesion_strength
+export (float, 0, 10, 0.2) var separation_strength := 1.5 setget set_separation_strength
+export var show_proximity_radius := true setget set_show_proximity_radius
+
+
+func _ready() -> void:
+ spawner.setup(
+ linear_speed_max,
+ linear_accel_max,
+ proximity_radius,
+ separation_decay_coefficient,
+ cohesion_strength,
+ separation_strength,
+ show_proximity_radius
+ )
+
+
+func set_linear_speed_max(value: float) -> void:
+ linear_speed_max = value
+ if not is_inside_tree():
+ return
+
+ spawner.set_linear_speed_max(value)
+
+
+func set_linear_accel_max(value: float) -> void:
+ linear_accel_max = value
+ if not is_inside_tree():
+ return
+
+ spawner.set_linear_accel_max(value)
+
+
+func set_proximity_radius(value: float) -> void:
+ proximity_radius = value
+ if not is_inside_tree():
+ return
+
+ spawner.set_proximity_radius(value)
+
+
+func set_show_proximity_radius(value: bool) -> void:
+ show_proximity_radius = value
+ if not is_inside_tree():
+ return
+
+ spawner.set_show_proximity_radius(value)
+
+
+func set_separation_decay_coef(value: float) -> void:
+ separation_decay_coefficient = value
+ if not is_inside_tree():
+ return
+
+ spawner.set_separation_decay_coef(value)
+
+
+func set_cohesion_strength(value: float) -> void:
+ cohesion_strength = value
+ if not is_inside_tree():
+ return
+
+ spawner.set_cohesion_strength(value)
+
+
+func set_separation_strength(value: float) -> void:
+ separation_strength = value
+ if not is_inside_tree():
+ return
+
+ spawner.set_separation_strength(value)
diff --git a/steering_ai/Demos/GroupBehaviors/GroupBehaviorsDemo.tscn b/steering_ai/Demos/GroupBehaviors/GroupBehaviorsDemo.tscn
new file mode 100644
index 0000000..2fb5437
--- /dev/null
+++ b/steering_ai/Demos/GroupBehaviors/GroupBehaviorsDemo.tscn
@@ -0,0 +1,29 @@
+[gd_scene load_steps=6 format=2]
+
+[ext_resource path="res://Demos/GroupBehaviors/Member.tscn" type="PackedScene" id=1]
+[ext_resource path="res://Demos/GroupBehaviors/Spawner.gd" type="Script" id=2]
+[ext_resource path="res://Demos/GroupBehaviors/GroupBehaviorsDemo.gd" type="Script" id=3]
+[ext_resource path="res://Demos/Utils/DemoInterface.tscn" type="PackedScene" id=4]
+[ext_resource path="res://Demos/Utils/BackgroudLayer.tscn" type="PackedScene" id=5]
+
+
+[node name="GroupBehaviorsDemo" type="Node"]
+script = ExtResource( 3 )
+linear_accel_max = 4234.0
+proximity_radius = 158.0
+separation_decay_coefficient = 121500.0
+cohesion_strength = 0.2
+separation_strength = 8.8
+
+[node name="BackgroudLayer" parent="." instance=ExtResource( 5 )]
+
+[node name="Spawner" type="Node2D" parent="."]
+position = Vector2( 973.261, 528.829 )
+script = ExtResource( 2 )
+member = ExtResource( 1 )
+
+[node name="DemoInterface" parent="." instance=ExtResource( 4 )]
+mouse_filter = 2
+text_bbcode = "Group Behavior Demo
+Each of the \"Agents\" are both attempting to stay separated from each other but within reach of their nearest group's center of mass.
+Click on agent to see it's proximity."
diff --git a/steering_ai/Demos/GroupBehaviors/Member.gd b/steering_ai/Demos/GroupBehaviors/Member.gd
new file mode 100644
index 0000000..6052d19
--- /dev/null
+++ b/steering_ai/Demos/GroupBehaviors/Member.gd
@@ -0,0 +1,69 @@
+extends KinematicBody2D
+
+var separation: GSAISeparation
+var cohesion: GSAICohesion
+var proximity: GSAIRadiusProximity
+var agent :GSAIKinematicBody2DAgent = null
+var blend : GSAIBlend = null
+var acceleration : GSAITargetAcceleration = null
+var draw_proximity := false
+
+var _color := Color.red
+var _velocity := Vector2()
+
+onready var collision_shape := $CollisionShape2D
+
+func _init() -> void:
+ agent = GSAIKinematicBody2DAgent.new()
+ agent.body = self
+
+ blend = GSAIBlend.new()
+ blend.agent = agent
+
+ acceleration = GSAITargetAcceleration.new()
+
+func setup(
+ linear_speed_max: float,
+ linear_accel_max: float,
+ proximity_radius: float,
+ separation_decay_coefficient: float,
+ cohesion_strength: float,
+ separation_strength: float
+) -> void:
+ _color = Color(rand_range(0.5, 1), rand_range(0.25, 1), rand_range(0, 1))
+ collision_shape.inner_color = _color
+
+ agent.linear_acceleration_max = linear_accel_max
+ agent.linear_speed_max = linear_speed_max
+ agent.linear_drag_percentage = 0.1
+
+ proximity = GSAIRadiusProximity.new()
+ proximity.agent = agent
+ proximity.radius = proximity_radius
+
+ separation = GSAISeparation.new()
+ separation.agent = agent
+ separation.proximity = proximity
+ separation.decay_coefficient = separation_decay_coefficient
+
+ cohesion = GSAICohesion.new()
+ cohesion.agent = agent
+ cohesion.proximity = proximity
+
+ blend.add_behavior(separation, separation_strength)
+ blend.add_behavior(cohesion, cohesion_strength)
+
+
+func _draw() -> void:
+ if draw_proximity:
+ draw_circle(Vector2.ZERO, proximity.radius, Color(0.4, 1.0, 0.89, 0.3))
+
+
+func _physics_process(delta: float) -> void:
+ if blend:
+ blend.calculate_steering(acceleration)
+ agent.apply_steering(acceleration, delta)
+
+
+func set_neighbors(neighbor: Array) -> void:
+ proximity.agents = neighbor
diff --git a/steering_ai/Demos/GroupBehaviors/Member.tscn b/steering_ai/Demos/GroupBehaviors/Member.tscn
new file mode 100644
index 0000000..7dd9ab8
--- /dev/null
+++ b/steering_ai/Demos/GroupBehaviors/Member.tscn
@@ -0,0 +1,18 @@
+[gd_scene load_steps=4 format=2]
+
+[ext_resource path="res://Demos/GroupBehaviors/Member.gd" type="Script" id=1]
+[ext_resource path="res://Demos/Utils/CircleDraw.gd" type="Script" id=3]
+
+
+[sub_resource type="CircleShape2D" id=1]
+radius = 16.0
+
+[node name="Member" type="KinematicBody2D"]
+input_pickable = true
+script = ExtResource( 1 )
+
+[node name="CollisionShape2D" type="CollisionShape2D" parent="."]
+shape = SubResource( 1 )
+script = ExtResource( 3 )
+outer_color = Color( 0.301961, 0.65098, 1, 1 )
+stroke = 4.0
diff --git a/steering_ai/Demos/GroupBehaviors/Spawner.gd b/steering_ai/Demos/GroupBehaviors/Spawner.gd
new file mode 100644
index 0000000..1fb8f2d
--- /dev/null
+++ b/steering_ai/Demos/GroupBehaviors/Spawner.gd
@@ -0,0 +1,92 @@
+extends Node2D
+
+export var member: PackedScene
+
+
+func follower_input_event(
+ viewport: Node,
+ event: InputEvent,
+ shape_idx: int,
+ follower: KinematicBody2D
+) -> void:
+ if event.is_action_pressed("click"):
+ for other in get_children():
+ if other.draw_proximity:
+ other.draw_proximity = false
+ other.update()
+ follower.draw_proximity = true
+ follower.update()
+ move_child(follower, get_child_count())
+
+
+func setup(
+ linear_speed_max: float,
+ linear_accel_max: float,
+ proximity_radius: float,
+ separation_decay_coefficient: float,
+ cohesion_strength: float,
+ separation_strength: float,
+ show_proximity_radius: bool
+) -> void:
+ var followers := []
+ for i in range(19):
+ var follower : KinematicBody2D = member.instance()
+ add_child(follower)
+ follower.position += Vector2(rand_range(-60, 60), rand_range(-60, 60))
+ followers.append(follower)
+ follower.setup(
+ linear_speed_max,
+ linear_accel_max,
+ proximity_radius,
+ separation_decay_coefficient,
+ cohesion_strength,
+ separation_strength
+ )
+
+ if i == 0 and show_proximity_radius:
+ follower.draw_proximity = true
+ follower.update()
+ follower.connect("input_event", self, "follower_input_event", [follower])
+
+ var agents := []
+ for i in followers:
+ agents.append(i.agent)
+ for i in followers:
+ i.proximity.agents = agents
+
+
+func set_linear_speed_max(value: float) -> void:
+ for child in get_children():
+ child.agent.linear_speed_max = value
+
+
+func set_linear_accel_max(value: float) -> void:
+ for child in get_children():
+ child.agent.linear_acceleration_max = value
+
+
+func set_proximity_radius(value: float) -> void:
+ for child in get_children():
+ child.proximity.radius = value
+ if child == get_child(0):
+ child.update()
+
+
+func set_show_proximity_radius(value: bool) -> void:
+ get_child(0).draw_proximity = value
+ get_child(0).update()
+
+
+func set_separation_decay_coef(value: float) -> void:
+ for child in get_children():
+ child.separation.decay_coefficient = value
+
+
+func set_cohesion_strength(value: float) -> void:
+ for child in get_children():
+ child.blend.get_behavior(1).weight = value
+
+
+func set_separation_strength(value: float) -> void:
+ for child in get_children():
+ child.blend.get_behavior(0).weight = value
diff --git a/steering_ai/Demos/PopulateItemList.gd b/steering_ai/Demos/PopulateItemList.gd
new file mode 100644
index 0000000..cd1bf6e
--- /dev/null
+++ b/steering_ai/Demos/PopulateItemList.gd
@@ -0,0 +1,68 @@
+extends ItemList
+
+signal demo_selected(scene_path)
+
+var file_paths := PoolStringArray()
+
+
+func _ready() -> void:
+ # warning-ignore:return_value_discarded
+ self.connect("item_selected", self, "_on_item_selected")
+
+ var this_directory: String = get_tree().current_scene.filename.rsplit("/", false, 1)[0]
+ file_paths = _find_files(this_directory, ["*Demo.tscn"], true)
+ populate(file_paths)
+ select(0)
+
+
+func populate(demos: PoolStringArray) -> void:
+ for path in demos:
+ var demo_name: String = path.rsplit("/", true, 1)[-1]
+ demo_name = demo_name.rsplit("Demo", true, 1)[0]
+ demo_name = sentencify(demo_name)
+ add_item(demo_name)
+
+
+func sentencify(line: String) -> String:
+ var regex := RegEx.new()
+ # warning-ignore:return_value_discarded
+ regex.compile("[A-Z]")
+
+ line = line.split(".", true, 1)[0]
+ line = regex.sub(line, " $0", true)
+ return line
+
+
+func _find_files(
+ dirpath := "", patterns := PoolStringArray(), is_recursive := false, do_skip_hidden := true
+) -> PoolStringArray:
+ var paths := PoolStringArray()
+ var directory := Directory.new()
+
+ if not directory.dir_exists(dirpath):
+ printerr("The directory does not exist: %s" % dirpath)
+ return paths
+ if not directory.open(dirpath) == OK:
+ printerr("Could not open the following dirpath: %s" % dirpath)
+ return paths
+
+ # warning-ignore:return_value_discarded
+ directory.list_dir_begin(true, do_skip_hidden)
+ var file_name := directory.get_next()
+ while file_name != "":
+ if directory.current_is_dir() and is_recursive:
+ var subdirectory := dirpath.plus_file(file_name)
+ paths.append_array(_find_files(subdirectory, patterns, is_recursive))
+ else:
+ for pattern in patterns:
+ if file_name.match(pattern):
+ paths.append(dirpath.plus_file(file_name))
+ file_name = directory.get_next()
+
+ directory.list_dir_end()
+ return paths
+
+
+func _on_item_selected(index: int) -> void:
+ var demo_path := file_paths[index]
+ emit_signal("demo_selected", demo_path)
diff --git a/steering_ai/Demos/PursueSeek/BoundaryManager.gd b/steering_ai/Demos/PursueSeek/BoundaryManager.gd
new file mode 100644
index 0000000..a5062cb
--- /dev/null
+++ b/steering_ai/Demos/PursueSeek/BoundaryManager.gd
@@ -0,0 +1,15 @@
+extends Node2D
+# Wraps the ships' positions around the world border.
+
+var _world_bounds: Vector2
+
+
+func _ready() -> void:
+ _world_bounds = Vector2(
+ ProjectSettings["display/window/size/width"], ProjectSettings["display/window/size/height"]
+ )
+
+
+func _physics_process(_delta: float) -> void:
+ for ship in get_children():
+ ship.position = ship.position.posmodv(_world_bounds)
diff --git a/steering_ai/Demos/PursueSeek/Player.gd b/steering_ai/Demos/PursueSeek/Player.gd
new file mode 100644
index 0000000..3557c36
--- /dev/null
+++ b/steering_ai/Demos/PursueSeek/Player.gd
@@ -0,0 +1,96 @@
+extends KinematicBody2D
+# Controls the player ship's movements based on player input.
+
+export var thruster_strength := 175.0
+export var side_thruster_strength := 10.0
+export var velocity_max := 300.0
+export var angular_velocity_max := 2.0
+export var angular_drag := 0.025
+export var linear_drag := 0.025
+
+var _linear_velocity := Vector2()
+var _angular_velocity := 0.0
+
+onready var agent := GSAISteeringAgent.new()
+
+
+func _physics_process(delta: float) -> void:
+ var movement := _get_movement()
+ _angular_velocity = _calculate_angular_velocity(
+ movement.x,
+ _angular_velocity,
+ side_thruster_strength,
+ angular_velocity_max,
+ angular_drag,
+ delta
+ )
+ rotation += _angular_velocity * delta
+
+ _linear_velocity = _calculate_linear_velocity(
+ movement.y,
+ _linear_velocity,
+ Vector2.UP.rotated(rotation),
+ linear_drag,
+ thruster_strength,
+ velocity_max,
+ delta
+ )
+
+ _linear_velocity = move_and_slide(_linear_velocity)
+ _update_agent()
+
+
+func _calculate_angular_velocity(
+ horizontal_movement: float,
+ current_velocity: float,
+ _thruster_strength: float,
+ _velocity_max: float,
+ ship_drag: float,
+ delta: float
+) -> float:
+ var velocity := clamp(
+ current_velocity + _thruster_strength * horizontal_movement * delta,
+ -_velocity_max,
+ _velocity_max
+ )
+
+ velocity = lerp(velocity, 0, ship_drag)
+
+ return velocity
+
+
+func _calculate_linear_velocity(
+ vertical_movement: float,
+ current_velocity: Vector2,
+ facing_direction: Vector2,
+ ship_drag_coefficient: float,
+ strength: float,
+ speed_max: float,
+ delta: float
+) -> Vector2:
+ var actual_strength := 0.0
+ if vertical_movement > 0:
+ actual_strength = strength
+ elif vertical_movement < 0:
+ actual_strength = -strength / 1.5
+
+ var velocity := current_velocity + facing_direction * actual_strength * delta
+ velocity = velocity.linear_interpolate(Vector2.ZERO, ship_drag_coefficient)
+
+ return velocity.limit_length(speed_max)
+
+
+func _get_movement() -> Vector2:
+ return Vector2(
+ Input.get_action_strength("sf_right") - Input.get_action_strength("sf_left"),
+ Input.get_action_strength("sf_up") - Input.get_action_strength("sf_down")
+ )
+
+
+func _update_agent() -> void:
+ agent.position.x = global_position.x
+ agent.position.y = global_position.y
+ agent.linear_velocity.x = _linear_velocity.x
+ agent.linear_velocity.y = _linear_velocity.y
+ agent.angular_velocity = _angular_velocity
+ agent.orientation = rotation
diff --git a/steering_ai/Demos/PursueSeek/PursueAndSeekDemo.gd b/steering_ai/Demos/PursueSeek/PursueAndSeekDemo.gd
new file mode 100644
index 0000000..e888235
--- /dev/null
+++ b/steering_ai/Demos/PursueSeek/PursueAndSeekDemo.gd
@@ -0,0 +1,39 @@
+extends Node
+
+export (float, 0, 2000, 40) var linear_speed_max := 120.0 setget set_linear_speed_max
+export (float, 0, 2000, 20) var linear_accel_max := 10.0 setget set_linear_accel_max
+export (float, 0, 5, 0.1) var predict_time := 1.0 setget set_predict_time
+
+onready var pursuer := $BoundaryManager/Pursuer
+onready var seeker := $BoundaryManager/Seeker
+
+
+func _ready() -> void:
+ pursuer.setup(predict_time, linear_speed_max, linear_accel_max)
+ seeker.setup(predict_time, linear_speed_max, linear_accel_max)
+
+
+func set_linear_speed_max(value: float) -> void:
+ linear_speed_max = value
+ if not is_inside_tree():
+ return
+
+ pursuer.agent.linear_speed_max = value
+ seeker.agent.linear_speed_max = value
+
+
+func set_linear_accel_max(value: float) -> void:
+ linear_accel_max = value
+ if not is_inside_tree():
+ return
+
+ pursuer.agent.linear_acceleration_max = value
+ seeker.agent.linear_acceleration_max = value
+
+
+func set_predict_time(value: float) -> void:
+ predict_time = value
+ if not is_inside_tree():
+ return
+
+ pursuer._behavior.predict_time_max = value
diff --git a/steering_ai/Demos/PursueSeek/PursueAndSeekDemo.tscn b/steering_ai/Demos/PursueSeek/PursueAndSeekDemo.tscn
new file mode 100644
index 0000000..2196f05
--- /dev/null
+++ b/steering_ai/Demos/PursueSeek/PursueAndSeekDemo.tscn
@@ -0,0 +1,92 @@
+[gd_scene load_steps=8 format=2]
+
+[ext_resource path="res://Demos/PursueSeek/Pursuer.gd" type="Script" id=1]
+[ext_resource path="res://Demos/PursueSeek/Player.gd" type="Script" id=2]
+[ext_resource path="res://Demos/PursueSeek/BoundaryManager.gd" type="Script" id=3]
+[ext_resource path="res://Demos/PursueSeek/PursueAndSeekDemo.gd" type="Script" id=4]
+[ext_resource path="res://Demos/Utils/DemoInterface.tscn" type="PackedScene" id=5]
+[ext_resource path="res://Demos/Utils/Line2DDraw.gd" type="Script" id=6]
+[ext_resource path="res://Demos/Utils/BackgroudLayer.tscn" type="PackedScene" id=7]
+
+[node name="PursueVSSeekDemo" type="Node"]
+script = ExtResource( 4 )
+linear_speed_max = 1280.0
+linear_accel_max = 1040.0
+predict_time = 1.1
+
+[node name="BackgroudLayer" parent="." instance=ExtResource( 7 )]
+
+[node name="BoundaryManager" type="Node2D" parent="."]
+script = ExtResource( 3 )
+
+[node name="Player" type="KinematicBody2D" parent="BoundaryManager"]
+position = Vector2( 307.552, 555.999 )
+rotation = 1.5708
+collision_mask = 2
+script = ExtResource( 2 )
+__meta__ = {
+"_edit_group_": true
+}
+thruster_strength = 1000.0
+side_thruster_strength = 40.0
+velocity_max = 450.0
+angular_velocity_max = 3.0
+
+[node name="CollisionPolygon2D" type="CollisionPolygon2D" parent="BoundaryManager/Player"]
+polygon = PoolVector2Array( 0, -32, -24, 32, 24, 32 )
+
+[node name="Line2D" type="Line2D" parent="BoundaryManager/Player"]
+points = PoolVector2Array( 0, 32, 24, 32, 0, -32, -24, 32, 0, 32 )
+width = 8.0
+default_color = Color( 0.560784, 0.870588, 0.364706, 1 )
+joint_mode = 2
+antialiased = true
+script = ExtResource( 6 )
+inner_color = Color( 0.235294, 0.639216, 0.439216, 1 )
+
+[node name="Pursuer" type="KinematicBody2D" parent="BoundaryManager"]
+position = Vector2( 1240.22, 866.784 )
+collision_layer = 2
+script = ExtResource( 1 )
+__meta__ = {
+"_edit_group_": true
+}
+
+[node name="CollisionPolygon2D" type="CollisionPolygon2D" parent="BoundaryManager/Pursuer"]
+polygon = PoolVector2Array( 0, -32, -24, 32, 24, 32 )
+
+[node name="Line2D" type="Line2D" parent="BoundaryManager/Pursuer"]
+points = PoolVector2Array( 0, 32, 24, 32, 0, -32, -24, 32, 0, 32 )
+width = 8.0
+default_color = Color( 1, 0.709804, 0.439216, 1 )
+joint_mode = 2
+antialiased = true
+script = ExtResource( 6 )
+inner_color = Color( 0.890196, 0.411765, 0.337255, 1 )
+
+[node name="Seeker" type="KinematicBody2D" parent="BoundaryManager"]
+position = Vector2( 1240.22, 280.108 )
+rotation = 3.14159
+collision_layer = 2
+script = ExtResource( 1 )
+__meta__ = {
+"_edit_group_": true
+}
+use_seek = true
+
+[node name="CollisionPolygon2D" type="CollisionPolygon2D" parent="BoundaryManager/Seeker"]
+polygon = PoolVector2Array( 0, -32, -24, 32, 24, 32 )
+
+[node name="Line2D" type="Line2D" parent="BoundaryManager/Seeker"]
+points = PoolVector2Array( 0, 32, 24, 32, 0, -32, -24, 32, 0, 32 )
+width = 8.0
+default_color = Color( 0.301961, 0.65098, 1, 1 )
+joint_mode = 2
+antialiased = true
+script = ExtResource( 6 )
+inner_color = Color( 0.294118, 0.356863, 0.670588, 1 )
+
+[node name="DemoInterface" parent="." instance=ExtResource( 5 )]
+text_bbcode = "Pursue vs. Seek Demo
+Move the player around with WASD and notice the [color=#ffb570]orange Pursuer[/color] and the [color=aqua]blue Seeker[/color] follow
+the [color=lime]green \"Ship\"[/color] around"
diff --git a/steering_ai/Demos/PursueSeek/Pursuer.gd b/steering_ai/Demos/PursueSeek/Pursuer.gd
new file mode 100644
index 0000000..1e4db40
--- /dev/null
+++ b/steering_ai/Demos/PursueSeek/Pursuer.gd
@@ -0,0 +1,80 @@
+extends KinematicBody2D
+# Represents a ship that chases after the player.
+
+export var use_seek: bool = false
+
+var _blend: GSAIBlend
+
+var _linear_drag_coefficient := 0.025
+var _angular_drag := 0.1
+var _direction_face := GSAIAgentLocation.new()
+
+var agent : GSAIKinematicBody2DAgent= null
+var accel : GSAITargetAcceleration = null
+var player_agent : GSAISteeringAgent = null
+
+
+func _ready() -> void:
+ agent = GSAIKinematicBody2DAgent.new()
+ agent.body = self
+
+ accel = GSAITargetAcceleration.new()
+ player_agent = owner.find_node("Player", true, false).agent
+
+ agent.calculate_velocities = false
+ set_physics_process(false)
+
+
+func _physics_process(delta: float) -> void:
+ _direction_face.position = agent.position + accel.linear.normalized()
+
+ _blend.calculate_steering(accel)
+
+ agent.angular_velocity = clamp(
+ agent.angular_velocity + accel.angular * delta, -agent.angular_speed_max, agent.angular_speed_max
+ )
+ agent.angular_velocity = lerp(agent.angular_velocity, 0, _angular_drag)
+
+ rotation += agent.angular_velocity * delta
+
+ var linear_velocity := (
+ GSAIUtils.to_vector2(agent.linear_velocity)
+ + (GSAIUtils.angle_to_vector2(rotation) * -agent.linear_acceleration_max * delta)
+ )
+ linear_velocity = linear_velocity.limit_length(agent.linear_speed_max)
+ linear_velocity = linear_velocity.linear_interpolate(Vector2.ZERO, _linear_drag_coefficient)
+
+ linear_velocity = move_and_slide(linear_velocity)
+ agent.linear_velocity = GSAIUtils.to_vector3(linear_velocity)
+
+
+func setup(predict_time: float, linear_speed_max: float, linear_accel_max: float) -> void:
+ var behavior: GSAISteeringBehavior
+ if use_seek:
+ behavior = GSAISeek.new()
+ behavior.agent = agent
+ behavior.target = player_agent
+ else:
+ behavior = GSAIPursue.new()
+ behavior.agent = agent
+ behavior.target = player_agent
+ behavior.predict_time_max = predict_time
+
+ var orient_behavior : GSAIFace = GSAIFace.new()
+ orient_behavior.agent = agent
+ orient_behavior.target = _direction_face
+
+ orient_behavior.alignment_tolerance = deg2rad(5)
+ orient_behavior.deceleration_radius = deg2rad(30)
+
+ _blend = GSAIBlend.new()
+ _blend.agent = agent
+ _blend.add_behavior(behavior, 1)
+ _blend.add_behavior(orient_behavior, 1)
+
+ agent.angular_acceleration_max = deg2rad(1080)
+ agent.angular_speed_max = deg2rad(360)
+ agent.linear_acceleration_max = linear_accel_max
+ agent.linear_speed_max = linear_speed_max
+
+ set_physics_process(true)
diff --git a/steering_ai/Demos/Quickstart/Agent.gd b/steering_ai/Demos/Quickstart/Agent.gd
new file mode 100644
index 0000000..c3cade1
--- /dev/null
+++ b/steering_ai/Demos/Quickstart/Agent.gd
@@ -0,0 +1,195 @@
+extends KinematicBody2D
+
+# Maximum possible linear velocity
+export var speed_max := 450.0
+# Maximum change in linear velocity
+export var acceleration_max := 50.0
+# Maximum rotation velocity represented in degrees
+export var angular_speed_max := 240
+# Maximum change in rotation velocity represented in degrees
+export var angular_acceleration_max := 40
+
+export var health_max := 100
+export var flee_health_threshold := 20
+
+var velocity := Vector2.ZERO
+var angular_velocity := 0.0
+var linear_drag := 0.1
+var angular_drag := 0.1
+
+# Holds the linear and angular components calculated by our steering behaviors.
+var acceleration : GSAITargetAcceleration = null
+
+onready var current_health := health_max
+
+# GSAISteeringAgent holds our agent's position, orientation, maximum speed and acceleration.
+var agent : GSAISteeringAgent = null
+
+var player: Node = null
+# This assumes that our player class will keep its own agent updated.
+var player_agent : GSAISteeringAgent = null
+
+# Proximities represent an area with which an agent can identify where neighbors in its relevant
+# group are. In our case, the group will feature the player, which will be used to avoid a
+# collision with them. We use a radius proximity so the player is only relevant inside 100 pixels.
+var proximity : GSAIRadiusProximity = null
+
+# GSAIBlend combines behaviors together, calculating all of their acceleration together and adding
+# them together, multiplied by a strength. We will have one for fleeing, and one for pursuing,
+# toggling them depending on the agent's health. Since we want the agent to rotate AND move, then
+# we aim to blend them together.
+var flee_blend : GSAIBlend = null
+var pursue_blend : GSAIBlend = null
+
+# GSAIPriority will be the main steering behavior we use. It holds sub-behaviors and will pick the
+# first one that returns non-zero acceleration, ignoring any afterwards.
+var priority : GSAIPriority = null
+
+
+func _ready() -> void:
+ acceleration = GSAITargetAcceleration.new()
+ agent = GSAISteeringAgent.new()
+ player = get_tree().get_nodes_in_group("Player")[0]
+ player_agent = player.agent
+
+ proximity = GSAIRadiusProximity.new()
+ proximity.agent = agent
+ proximity.agents = [ player_agent ]
+ proximity.radius = 100
+
+ flee_blend = GSAIBlend.new()
+ flee_blend.agent = agent
+
+ pursue_blend = GSAIBlend.new()
+ pursue_blend.agent = agent
+
+ priority = GSAIPriority.new()
+ priority.agent = agent
+
+ # ---------- Configuration for our agent ----------
+ agent.linear_speed_max = speed_max
+ agent.linear_acceleration_max = acceleration_max
+ agent.angular_speed_max = deg2rad(angular_speed_max)
+ agent.angular_acceleration_max = deg2rad(angular_acceleration_max)
+ agent.bounding_radius = calculate_radius($CollisionPolygon2D.polygon)
+ update_agent()
+
+ # ---------- Configuration for our behaviors ----------
+ # Pursue will happen while the agent is in good health. It produces acceleration that takes
+ # the agent on an intercept course with the target, predicting its position in the future.
+ var pursue : GSAIPursue = GSAIPursue.new()
+ pursue.agent = agent
+ pursue.target = player_agent
+ pursue.predict_time_max = 1.5
+
+ # Flee will happen while the agent is in bad health, so will start disabled. It produces
+ # acceleration that takes the agent directly away from the target with no prediction.
+ var flee : GSAIFlee = GSAIFlee.new()
+ flee.agent = agent
+ flee.target = player_agent
+
+ # AvoidCollision tries to keep the agent from running into any of the neighbors found in its
+ # proximity group. In our case, this will be the player, if they are close enough.
+ var avoid : GSAIAvoidCollisions = GSAIAvoidCollisions.new()
+ avoid.agent = agent
+ avoid.proximity = proximity
+
+ # Face turns the agent to keep looking towards its target. It will be enabled while the agent
+ # is not fleeing due to low health. It tries to arrive 'on alignment' with 0 remaining velocity.
+ var face : GSAIFace = GSAIFace.new()
+ face.agent = agent
+ face.target = player_agent
+
+ # We use deg2rad because the math in the toolkit assumes radians.
+ # How close for the agent to be 'aligned', if not exact.
+ face.alignment_tolerance = deg2rad(5)
+ # When to start slowing down
+ face.deceleration_radius = deg2rad(60)
+
+ # LookWhereYouGo turns the agent to keep looking towards its direction of travel. It will only
+ # be enabled while the agent is at low health.
+ var look : GSAILookWhereYouGo = GSAILookWhereYouGo.new()
+ look.agent = agent
+ # How close for the agent to be 'aligned', if not exact
+ look.alignment_tolerance = deg2rad(5)
+ # When to start slowing down.
+ look.deceleration_radius = deg2rad(60)
+
+ # Behaviors that are not enabled produce 0 acceleration.
+ # Adding our fleeing behaviors to a blend. The order does not matter.
+ flee_blend.is_enabled = false
+ flee_blend.add_behavior(look, 1)
+ flee_blend.add_behavior(flee, 1)
+
+ # Adding our pursuit behaviors to a blend. The order does not matter.
+ pursue_blend.add_behavior(face, 1)
+ pursue_blend.add_behavior(pursue, 1)
+
+ # Adding our final behaviors to the main priority behavior. The order does matter here.
+ # We want to avoid collision with the player first, flee from the player second when enabled,
+ # and pursue the player last when enabled.
+ priority.add_behavior(avoid)
+ priority.add_behavior(flee_blend)
+ priority.add_behavior(pursue_blend)
+
+
+func _physics_process(delta: float) -> void:
+ # Make sure any change in position and speed has been recorded.
+ update_agent()
+
+ if current_health <= flee_health_threshold:
+ pursue_blend.is_enabled = false
+ flee_blend.is_enabled = true
+
+ # Calculate the desired acceleration.
+ priority.calculate_steering(acceleration)
+
+ # We add the discovered acceleration to our linear velocity. The toolkit does not limit
+ # velocity, just acceleration, so we clamp the result ourselves here.
+ velocity = (velocity + Vector2(acceleration.linear.x, acceleration.linear.y) * delta).limit_length(
+ agent.linear_speed_max
+ )
+
+ # This applies drag on the agent's motion, helping it to slow down naturally.
+ velocity = velocity.linear_interpolate(Vector2.ZERO, linear_drag)
+
+ # And since we're using a KinematicBody2D, we use Godot's excellent move_and_slide to actually
+ # apply the final movement, and record any change in velocity the physics engine discovered.
+ velocity = move_and_slide(velocity)
+
+ # We then do something similar to apply our agent's rotational speed.
+ angular_velocity = clamp(
+ angular_velocity + acceleration.angular * delta, -agent.angular_speed_max, agent.angular_speed_max
+ )
+ # This applies drag on the agent's rotation, helping it slow down naturally.
+ angular_velocity = lerp(angular_velocity, 0, angular_drag)
+ rotation += angular_velocity * delta
+
+
+# In order to support both 2D and 3D, the toolkit uses Vector3, so the conversion is required
+# when using 2D nodes. The Z component can be left to 0 safely.
+func update_agent() -> void:
+ agent.position.x = global_position.x
+ agent.position.y = global_position.y
+ agent.orientation = rotation
+ agent.linear_velocity.x = velocity.x
+ agent.linear_velocity.y = velocity.y
+ agent.angular_velocity = angular_velocity
+
+
+# We calculate the radius from the collision shape - this will approximate the agent's size in the
+# game world, to avoid collisions with the player.
+func calculate_radius(polygon: PoolVector2Array) -> float:
+ var furthest_point := Vector2(-INF, -INF)
+ for p in polygon:
+ if abs(p.x) > furthest_point.x:
+ furthest_point.x = p.x
+ if abs(p.y) > furthest_point.y:
+ furthest_point.y = p.y
+ return furthest_point.length()
+
+
+func damage(amount: int) -> void:
+ current_health -= amount
+ if current_health <= 0:
+ queue_free()
diff --git a/steering_ai/Demos/Quickstart/Bullet.gd b/steering_ai/Demos/Quickstart/Bullet.gd
new file mode 100644
index 0000000..a33fc8f
--- /dev/null
+++ b/steering_ai/Demos/Quickstart/Bullet.gd
@@ -0,0 +1,33 @@
+extends KinematicBody2D
+
+export var speed := 1500.0
+
+var velocity := Vector2.ZERO
+var player: Node
+
+onready var timer := $Lifetime
+
+
+func _ready() -> void:
+ timer.connect("timeout", self, "_on_Lifetime_timeout")
+ timer.start()
+
+
+func _physics_process(delta: float) -> void:
+ var collision := move_and_collide(velocity * delta)
+ if collision:
+ timer.stop()
+ clear()
+ collision.collider.damage(10)
+
+
+func start(direction: Vector2) -> void:
+ velocity = direction * speed
+
+
+func clear() -> void:
+ queue_free()
+
+
+func _on_Lifetime_timeout() -> void:
+ clear()
diff --git a/steering_ai/Demos/Quickstart/Bullet.tscn b/steering_ai/Demos/Quickstart/Bullet.tscn
new file mode 100644
index 0000000..60ceb8d
--- /dev/null
+++ b/steering_ai/Demos/Quickstart/Bullet.tscn
@@ -0,0 +1,23 @@
+[gd_scene load_steps=4 format=2]
+
+[ext_resource path="res://Demos/Utils/CircleDraw.gd" type="Script" id=1]
+[ext_resource path="res://Demos/Quickstart/Bullet.gd" type="Script" id=2]
+
+[sub_resource type="CircleShape2D" id=1]
+radius = 4.0
+
+[node name="Bullet" type="KinematicBody2D"]
+collision_layer = 4
+collision_mask = 2
+script = ExtResource( 2 )
+
+[node name="CollisionShape2D" type="CollisionShape2D" parent="."]
+shape = SubResource( 1 )
+script = ExtResource( 1 )
+inner_color = Color( 0.235294, 0.639216, 0.439216, 1 )
+outer_color = Color( 0.560784, 0.870588, 0.364706, 1 )
+stroke = 2.0
+
+[node name="Lifetime" type="Timer" parent="."]
+process_mode = 0
+wait_time = 3.0
diff --git a/steering_ai/Demos/Quickstart/Player.gd b/steering_ai/Demos/Quickstart/Player.gd
new file mode 100644
index 0000000..7eb518d
--- /dev/null
+++ b/steering_ai/Demos/Quickstart/Player.gd
@@ -0,0 +1,99 @@
+extends KinematicBody2D
+
+export var speed_max := 650.0
+export var acceleration_max := 70.0
+export var rotation_speed_max := 240
+export var rotation_accel_max := 40
+export var bullet: PackedScene
+
+var velocity := Vector2.ZERO
+var angular_velocity := 0.0
+var direction := Vector2.RIGHT
+
+var agent : GSAISteeringAgent = null
+var proxy_target : GSAIAgentLocation = null
+var face : GSAIFace = null
+
+onready var accel := GSAITargetAcceleration.new()
+onready var bullets := owner.get_node("Bullets")
+
+
+func _ready() -> void:
+ agent = GSAISteeringAgent.new()
+ proxy_target = GSAIAgentLocation.new()
+ face = GSAIFace.new()
+ face.agent = agent
+ face.target = proxy_target
+
+ agent.linear_speed_max = speed_max
+ agent.linear_acceleration_max = acceleration_max
+ agent.angular_speed_max = deg2rad(rotation_speed_max)
+ agent.angular_acceleration_max = deg2rad(rotation_accel_max)
+ agent.bounding_radius = calculate_radius($CollisionPolygon2D.polygon)
+ update_agent()
+
+ var mouse_pos := get_global_mouse_position()
+ proxy_target.position.x = mouse_pos.x
+ proxy_target.position.y = mouse_pos.y
+
+ face.alignment_tolerance = deg2rad(5)
+ face.deceleration_radius = deg2rad(45)
+
+
+func _physics_process(delta: float) -> void:
+ update_agent()
+
+ var movement := get_movement()
+
+ direction = GSAIUtils.angle_to_vector2(rotation)
+
+ velocity += direction * acceleration_max * movement * delta
+ velocity = velocity.limit_length(speed_max)
+ velocity = velocity.linear_interpolate(Vector2.ZERO, 0.1)
+ velocity = move_and_slide(velocity)
+
+ face.calculate_steering(accel)
+ angular_velocity += accel.angular * delta
+ angular_velocity = clamp(angular_velocity, -agent.angular_speed_max, agent.angular_speed_max)
+ angular_velocity = lerp(angular_velocity, 0, 0.1)
+ rotation += angular_velocity * delta
+
+
+func _unhandled_input(event: InputEvent) -> void:
+ if event is InputEventMouseMotion:
+ var mouse_pos: Vector2 = event.position
+ proxy_target.position.x = mouse_pos.x
+ proxy_target.position.y = mouse_pos.y
+ elif event is InputEventMouseButton:
+ if event.button_index == BUTTON_LEFT and event.pressed:
+ var next_bullet := bullet.instance()
+ next_bullet.global_position = (
+ global_position
+ - direction * (agent.bounding_radius - 5)
+ )
+ next_bullet.player = self
+ next_bullet.start(-direction)
+ bullets.add_child(next_bullet)
+
+
+func get_movement() -> float:
+ return Input.get_action_strength("sf_down") - Input.get_action_strength("sf_up")
+
+
+func update_agent() -> void:
+ agent.position.x = global_position.x
+ agent.position.y = global_position.y
+ agent.orientation = rotation
+ agent.linear_velocity.x = velocity.x
+ agent.linear_velocity.y = velocity.y
+ agent.angular_velocity = angular_velocity
+
+
+func calculate_radius(polygon: PoolVector2Array) -> float:
+ var furthest_point := Vector2(-INF, -INF)
+ for p in polygon:
+ if abs(p.x) > furthest_point.x:
+ furthest_point.x = p.x
+ if abs(p.y) > furthest_point.y:
+ furthest_point.y = p.y
+ return furthest_point.length()
diff --git a/steering_ai/Demos/Quickstart/QuickStartDemo.tscn b/steering_ai/Demos/Quickstart/QuickStartDemo.tscn
new file mode 100644
index 0000000..19e2854
--- /dev/null
+++ b/steering_ai/Demos/Quickstart/QuickStartDemo.tscn
@@ -0,0 +1,61 @@
+[gd_scene load_steps=6 format=2]
+
+[ext_resource path="res://Demos/Utils/Line2DDraw.gd" type="Script" id=1]
+[ext_resource path="res://Demos/Quickstart/Agent.gd" type="Script" id=2]
+[ext_resource path="res://Demos/Quickstart/Player.gd" type="Script" id=3]
+[ext_resource path="res://Demos/Quickstart/Bullet.tscn" type="PackedScene" id=4]
+[ext_resource path="res://Demos/Utils/BackgroudLayer.tscn" type="PackedScene" id=5]
+
+[node name="QuickStartDemo" type="Node"]
+
+[node name="BackgroudLayer" parent="." instance=ExtResource( 5 )]
+
+[node name="Player" type="KinematicBody2D" parent="." groups=[
+"Player",
+]]
+position = Vector2( 402.346, 573.791 )
+rotation = 1.5708
+collision_mask = 2
+script = ExtResource( 3 )
+speed_max = 900.0
+acceleration_max = 4200.0
+rotation_speed_max = 360
+rotation_accel_max = 1280
+bullet = ExtResource( 4 )
+
+[node name="CollisionPolygon2D" type="CollisionPolygon2D" parent="Player"]
+polygon = PoolVector2Array( 0, -32, -24, 32, 24, 32 )
+
+[node name="Line2D" type="Line2D" parent="Player"]
+points = PoolVector2Array( 0, 32, 24, 32, 0, -32, -24, 32, 0, 32 )
+width = 8.0
+default_color = Color( 0.560784, 0.870588, 0.364706, 1 )
+joint_mode = 2
+antialiased = true
+script = ExtResource( 1 )
+inner_color = Color( 0.235294, 0.639216, 0.439216, 1 )
+
+[node name="Agent" type="KinematicBody2D" parent="."]
+position = Vector2( 974.675, 266.224 )
+rotation = 1.5708
+collision_layer = 2
+collision_mask = 5
+script = ExtResource( 2 )
+speed_max = 600.0
+acceleration_max = 2800.0
+angular_speed_max = 360
+angular_acceleration_max = 1280
+
+[node name="CollisionPolygon2D" type="CollisionPolygon2D" parent="Agent"]
+polygon = PoolVector2Array( 0, -32, -24, 32, 24, 32 )
+
+[node name="Line2D" type="Line2D" parent="Agent"]
+points = PoolVector2Array( 0, 32, 24, 32, 0, -32, -24, 32, 0, 32 )
+width = 8.0
+default_color = Color( 1, 0.709804, 0.439216, 1 )
+joint_mode = 2
+antialiased = true
+script = ExtResource( 1 )
+inner_color = Color( 0.890196, 0.411765, 0.337255, 1 )
+
+[node name="Bullets" type="Node2D" parent="."]
diff --git a/steering_ai/Demos/SeekFlee/Boundaries.gd b/steering_ai/Demos/SeekFlee/Boundaries.gd
new file mode 100644
index 0000000..827671f
--- /dev/null
+++ b/steering_ai/Demos/SeekFlee/Boundaries.gd
@@ -0,0 +1,32 @@
+extends Node2D
+
+const COLOR := Color("8fde5d")
+
+
+func _ready() -> void:
+ get_tree().root.connect("size_changed", self, "_on_SceneTree_size_changed")
+ _on_SceneTree_size_changed()
+
+
+func _draw() -> void:
+ for b in get_children():
+ var extents: Vector2 = b.get_node("CollisionShape2D").shape.extents
+ draw_rect(Rect2(b.global_position - extents, extents * 2), COLOR)
+
+
+func _on_SceneTree_size_changed() -> void:
+ var size := Vector2(
+ ProjectSettings["display/window/size/width"], ProjectSettings["display/window/size/height"]
+ )
+ for b in get_children():
+ var boundary: String = str(b.name).rsplit("Boundary")[0]
+ match boundary:
+ "Left":
+ b.global_position = Vector2(0, size.y / 2)
+ "Right":
+ b.global_position = Vector2(size.x, size.y / 2)
+ "Top":
+ b.global_position = Vector2(size.x / 2, 0)
+ "Bottom":
+ b.global_position = Vector2(size.x / 2, size.y)
+ update()
diff --git a/steering_ai/Demos/SeekFlee/Player.gd b/steering_ai/Demos/SeekFlee/Player.gd
new file mode 100644
index 0000000..7008704
--- /dev/null
+++ b/steering_ai/Demos/SeekFlee/Player.gd
@@ -0,0 +1,26 @@
+extends KinematicBody2D
+# Class to control the player in basic left/right up/down movement.
+
+var speed: float
+onready var agent := GSAIAgentLocation.new()
+
+
+func _ready() -> void:
+ agent.position = GSAIUtils.to_vector3(global_position)
+
+
+func _physics_process(_delta: float) -> void:
+ var movement := _get_movement()
+ if movement.length_squared() < 0.01:
+ return
+
+ # warning-ignore:return_value_discarded
+ move_and_slide(movement * speed)
+ agent.position = GSAIUtils.to_vector3(global_position)
+
+
+func _get_movement() -> Vector2:
+ return Vector2(
+ Input.get_action_strength("sf_right") - Input.get_action_strength("sf_left"),
+ Input.get_action_strength("sf_down") - Input.get_action_strength("sf_up")
+ )
diff --git a/steering_ai/Demos/SeekFlee/SeekFleeDemo.gd b/steering_ai/Demos/SeekFlee/SeekFleeDemo.gd
new file mode 100644
index 0000000..027693c
--- /dev/null
+++ b/steering_ai/Demos/SeekFlee/SeekFleeDemo.gd
@@ -0,0 +1,82 @@
+extends Node
+# Access helper class for children to access window boundaries.
+
+enum Mode { FLEE, SEEK }
+
+export (Mode) var behavior_mode := Mode.SEEK setget set_behavior_mode
+export (float, 0, 1000, 30) var linear_speed_max := 200.0 setget set_linear_speed_max
+export (float, 0, 2000, 40) var linear_accel_max := 10.0 setget set_linear_accel_max
+export (float) var player_speed := 600.0 setget set_player_speed
+
+var camera_boundaries: Rect2
+
+onready var player: KinematicBody2D = $Player
+onready var spawner: Node2D = $Spawner
+
+
+func _ready() -> void:
+ camera_boundaries = Rect2(
+ Vector2.ZERO,
+ Vector2(
+ ProjectSettings["display/window/size/width"],
+ ProjectSettings["display/window/size/height"]
+ )
+ )
+
+ var rng := RandomNumberGenerator.new()
+ rng.randomize()
+
+ player.speed = player_speed
+
+ for i in range(spawner.entity_count):
+ var new_pos := Vector2(
+ rng.randf_range(0, camera_boundaries.size.x),
+ rng.randf_range(0, camera_boundaries.size.y)
+ )
+ var entity: KinematicBody2D = spawner.Entity.instance()
+ entity.global_position = new_pos
+ entity.player_agent = player.agent
+ entity.start_speed = linear_speed_max
+ entity.start_accel = linear_accel_max
+ entity.use_seek = behavior_mode == Mode.SEEK
+ spawner.add_child(entity)
+
+
+func set_behavior_mode(mode: int) -> void:
+ behavior_mode = mode
+ if not is_inside_tree():
+ return
+
+ match mode:
+ Mode.SEEK:
+ for child in spawner.get_children():
+ child.use_seek = true
+ Mode.FLEE:
+ for child in spawner.get_children():
+ child.use_seek = false
+
+
+func set_linear_speed_max(value: float) -> void:
+ linear_speed_max = value
+ if not is_inside_tree():
+ return
+
+ for child in spawner.get_children():
+ child.agent.linear_speed_max = value
+
+
+func set_linear_accel_max(value: float) -> void:
+ linear_accel_max = value
+ if not is_inside_tree():
+ return
+
+ for child in spawner.get_children():
+ child.agent.linear_acceleration_max = value
+
+
+func set_player_speed(value: float) -> void:
+ player_speed = value
+ if not is_inside_tree():
+ return
+
+ player.speed = player_speed
diff --git a/steering_ai/Demos/SeekFlee/SeekFleeDemo.tscn b/steering_ai/Demos/SeekFlee/SeekFleeDemo.tscn
new file mode 100644
index 0000000..2b75042
--- /dev/null
+++ b/steering_ai/Demos/SeekFlee/SeekFleeDemo.tscn
@@ -0,0 +1,81 @@
+[gd_scene load_steps=12 format=2]
+
+[ext_resource path="res://Demos/SeekFlee/Player.gd" type="Script" id=2]
+[ext_resource path="res://Demos/SeekFlee/SeekFleeDemo.gd" type="Script" id=3]
+[ext_resource path="res://Demos/SeekFlee/Spawner.gd" type="Script" id=4]
+[ext_resource path="res://Demos/Utils/DemoInterface.tscn" type="PackedScene" id=5]
+[ext_resource path="res://Demos/SeekFlee/Seeker.tscn" type="PackedScene" id=6]
+[ext_resource path="res://Demos/Utils/CircleDraw.gd" type="Script" id=7]
+[ext_resource path="res://Demos/Utils/BackgroudLayer.tscn" type="PackedScene" id=8]
+[ext_resource path="res://Demos/SeekFlee/Boundaries.gd" type="Script" id=9]
+
+[sub_resource type="CircleShape2D" id=1]
+radius = 32.0
+
+[sub_resource type="RectangleShape2D" id=2]
+extents = Vector2( 10, 542 )
+
+[sub_resource type="RectangleShape2D" id=3]
+extents = Vector2( 965.654, 10 )
+
+[node name="SeekFleeDemo" type="Node"]
+script = ExtResource( 3 )
+linear_speed_max = 570.0
+linear_accel_max = 1160.0
+
+[node name="BackgroudLayer" parent="." instance=ExtResource( 8 )]
+
+[node name="Player" type="KinematicBody2D" parent="."]
+position = Vector2( 960, 540 )
+collision_mask = 2
+script = ExtResource( 2 )
+
+[node name="CollisionShape2D" type="CollisionShape2D" parent="Player"]
+shape = SubResource( 1 )
+script = ExtResource( 7 )
+inner_color = Color( 0.235294, 0.639216, 0.439216, 1 )
+outer_color = Color( 0.560784, 0.870588, 0.364706, 1 )
+stroke = 4.0
+
+[node name="Boundaries" type="Node2D" parent="."]
+script = ExtResource( 9 )
+
+[node name="LeftBoundary" type="StaticBody2D" parent="Boundaries"]
+position = Vector2( 0, 540 )
+collision_layer = 2
+collision_mask = 5
+
+[node name="CollisionShape2D" type="CollisionShape2D" parent="Boundaries/LeftBoundary"]
+shape = SubResource( 2 )
+
+[node name="RightBoundary" type="StaticBody2D" parent="Boundaries"]
+position = Vector2( 1920, 540 )
+collision_layer = 2
+collision_mask = 5
+
+[node name="CollisionShape2D" type="CollisionShape2D" parent="Boundaries/RightBoundary"]
+shape = SubResource( 2 )
+
+[node name="TopBoundary" type="StaticBody2D" parent="Boundaries"]
+position = Vector2( 960, 0 )
+collision_layer = 2
+collision_mask = 5
+
+[node name="CollisionShape2D" type="CollisionShape2D" parent="Boundaries/TopBoundary"]
+shape = SubResource( 3 )
+
+[node name="BottomBoundary" type="StaticBody2D" parent="Boundaries"]
+position = Vector2( 960, 1080 )
+collision_layer = 2
+collision_mask = 5
+
+[node name="CollisionShape2D" type="CollisionShape2D" parent="Boundaries/BottomBoundary"]
+shape = SubResource( 3 )
+
+[node name="Spawner" type="Node2D" parent="."]
+script = ExtResource( 4 )
+Entity = ExtResource( 6 )
+
+[node name="DemoInterface" parent="." instance=ExtResource( 5 )]
+text_bbcode = "Seek & Flee Demo
+Move the [color=lime]green \"Player\"[/color] around with WASD and notice the [color=#ffb570]orange \"Enemies\"[/color] try to seek to or flee from the player."
diff --git a/steering_ai/Demos/SeekFlee/Seeker.gd b/steering_ai/Demos/SeekFlee/Seeker.gd
new file mode 100644
index 0000000..21878ee
--- /dev/null
+++ b/steering_ai/Demos/SeekFlee/Seeker.gd
@@ -0,0 +1,42 @@
+extends KinematicBody2D
+
+var player_agent: GSAIAgentLocation
+var velocity := Vector2.ZERO
+var start_speed: float
+var start_accel: float
+var use_seek := true
+
+var agent : GSAIKinematicBody2DAgent = null
+var accel : GSAITargetAcceleration = null
+var seek : GSAISeek = null
+var flee : GSAIFlee = null
+
+
+func _ready() -> void:
+ agent = GSAIKinematicBody2DAgent.new()
+ agent.body = self
+
+ accel = GSAITargetAcceleration.new()
+
+ seek = GSAISeek.new()
+ seek.agent = agent
+ seek.target = player_agent
+
+ flee = GSAIFlee.new()
+ flee.agent = agent
+ flee.target = player_agent
+
+ agent.linear_acceleration_max = start_accel
+ agent.linear_speed_max = start_speed
+
+
+func _physics_process(delta: float) -> void:
+ if not player_agent:
+ return
+
+ if use_seek:
+ seek.calculate_steering(accel)
+ else:
+ flee.calculate_steering(accel)
+
+ agent.apply_steering(accel, delta)
diff --git a/steering_ai/Demos/SeekFlee/Seeker.tscn b/steering_ai/Demos/SeekFlee/Seeker.tscn
new file mode 100644
index 0000000..e65c072
--- /dev/null
+++ b/steering_ai/Demos/SeekFlee/Seeker.tscn
@@ -0,0 +1,19 @@
+[gd_scene load_steps=4 format=2]
+
+[ext_resource path="res://Demos/SeekFlee/Seeker.gd" type="Script" id=1]
+[ext_resource path="res://Demos/Utils/CircleDraw.gd" type="Script" id=2]
+
+[sub_resource type="CircleShape2D" id=1]
+radius = 16.0
+
+[node name="Seeker" type="KinematicBody2D"]
+collision_layer = 4
+collision_mask = 2
+script = ExtResource( 1 )
+
+[node name="CollisionShape2D" type="CollisionShape2D" parent="."]
+shape = SubResource( 1 )
+script = ExtResource( 2 )
+inner_color = Color( 0.890196, 0.411765, 0.337255, 1 )
+outer_color = Color( 1, 0.709804, 0.439216, 1 )
+stroke = 4.0
diff --git a/steering_ai/Demos/SeekFlee/Spawner.gd b/steering_ai/Demos/SeekFlee/Spawner.gd
new file mode 100644
index 0000000..abd3929
--- /dev/null
+++ b/steering_ai/Demos/SeekFlee/Spawner.gd
@@ -0,0 +1,6 @@
+extends Node2D
+# Holds data to instantiate and configure a number of agent entities.
+
+export (PackedScene) var Entity: PackedScene
+export var entity_count := 10
+export var entity_color := Color.blue
diff --git a/steering_ai/Demos/Utils/BackgroudLayer.tscn b/steering_ai/Demos/Utils/BackgroudLayer.tscn
new file mode 100644
index 0000000..9b6a8f1
--- /dev/null
+++ b/steering_ai/Demos/Utils/BackgroudLayer.tscn
@@ -0,0 +1,17 @@
+[gd_scene load_steps=2 format=2]
+
+[ext_resource path="res://assets/sprites/background.png" type="Texture" id=1]
+
+[node name="BackgroudLayer" type="CanvasLayer"]
+layer = -1
+
+[node name="Background" type="TextureRect" parent="."]
+anchor_right = 1.0
+anchor_bottom = 1.0
+mouse_filter = 2
+texture = ExtResource( 1 )
+expand = true
+stretch_mode = 7
+__meta__ = {
+"_edit_use_anchors_": false
+}
diff --git a/steering_ai/Demos/Utils/CircleDraw.gd b/steering_ai/Demos/Utils/CircleDraw.gd
new file mode 100644
index 0000000..1c51f05
--- /dev/null
+++ b/steering_ai/Demos/Utils/CircleDraw.gd
@@ -0,0 +1,26 @@
+tool
+extends CollisionShape2D
+
+export (Color) var inner_color := Color() setget set_inner_color
+export (Color) var outer_color := Color() setget set_outer_color
+export (float) var stroke := 0.0 setget set_stroke
+
+
+func _draw() -> void:
+ draw_circle(Vector2.ZERO, shape.radius + stroke, outer_color)
+ draw_circle(Vector2.ZERO, shape.radius, inner_color)
+
+
+func set_inner_color(val: Color) -> void:
+ inner_color = val
+ update()
+
+
+func set_outer_color(val: Color) -> void:
+ outer_color = val
+ update()
+
+
+func set_stroke(val: float) -> void:
+ stroke = val
+ update()
diff --git a/steering_ai/Demos/Utils/DemoInterface.gd b/steering_ai/Demos/Utils/DemoInterface.gd
new file mode 100644
index 0000000..93b7082
--- /dev/null
+++ b/steering_ai/Demos/Utils/DemoInterface.gd
@@ -0,0 +1,13 @@
+tool
+extends PanelContainer
+
+export (String, MULTILINE) var text_bbcode := "" setget set_text_bbcode
+
+onready var rich_text_label: RichTextLabel = $MarginContainer/RichTextLabel
+
+
+func set_text_bbcode(value: String) -> void:
+ text_bbcode = value
+ if not rich_text_label:
+ yield(self, "ready")
+ rich_text_label.bbcode_text = text_bbcode
diff --git a/steering_ai/Demos/Utils/DemoInterface.tscn b/steering_ai/Demos/Utils/DemoInterface.tscn
new file mode 100644
index 0000000..7855926
--- /dev/null
+++ b/steering_ai/Demos/Utils/DemoInterface.tscn
@@ -0,0 +1,37 @@
+[gd_scene load_steps=4 format=2]
+
+[ext_resource path="res://assets/theme/gdquest.theme" type="Theme" id=1]
+[ext_resource path="res://Demos/Utils/DemoInterface.gd" type="Script" id=2]
+
+[sub_resource type="GDScript" id=1]
+script/source = "tool
+extends RichTextLabel
+
+"
+
+[node name="DemoInterface" type="PanelContainer"]
+anchor_right = 1.0
+margin_bottom = 140.0
+rect_min_size = Vector2( 1024, 0 )
+theme = ExtResource( 1 )
+script = ExtResource( 2 )
+__meta__ = {
+"_edit_use_anchors_": false
+}
+text_bbcode = "Replace this text for the demo."
+
+[node name="MarginContainer" type="MarginContainer" parent="."]
+margin_right = 1920.0
+margin_bottom = 140.0
+
+[node name="RichTextLabel" type="RichTextLabel" parent="MarginContainer"]
+margin_left = 16.0
+margin_top = 16.0
+margin_right = 1904.0
+margin_bottom = 124.0
+rect_min_size = Vector2( 0, 55 )
+bbcode_enabled = true
+bbcode_text = "Replace this text for the demo."
+text = "Replace this text for the demo."
+scroll_active = false
+script = SubResource( 1 )
diff --git a/steering_ai/Demos/Utils/Line2DDraw.gd b/steering_ai/Demos/Utils/Line2DDraw.gd
new file mode 100644
index 0000000..d1a7b5e
--- /dev/null
+++ b/steering_ai/Demos/Utils/Line2DDraw.gd
@@ -0,0 +1,13 @@
+tool
+extends Line2D
+
+export (Color) var inner_color := Color() setget set_inner_color
+
+
+func _draw() -> void:
+ draw_colored_polygon(points, inner_color)
+
+
+func set_inner_color(val: Color) -> void:
+ inner_color = val
+ update()
diff --git a/steering_ai/LICENSE b/steering_ai/LICENSE
new file mode 100644
index 0000000..f3e861c
--- /dev/null
+++ b/steering_ai/LICENSE
@@ -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.
diff --git a/steering_ai/README.md b/steering_ai/README.md
new file mode 100644
index 0000000..a8c89df
--- /dev/null
+++ b/steering_ai/README.md
@@ -0,0 +1,70 @@
+# Godot Steering AI Framework
+
+![Project banner](./assets/banner.svg)
+
+This project is a framework to code complex and smooth AI movement in the [Godot game engine](https://godotengine.org/), in GDScript, using steering behaviors. It works in **both 2D and 3D games**.
+
+➡ Follow us on [Twitter](https://twitter.com/NathanGDQuest) and [YouTube](https://www.youtube.com/c/gdquest/) for free game creation tutorials, tips, and news! Get one of our [Godot game creation courses](https://gdquest.mavenseed.com/) to support our work on Free Software.
+
+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.
+
+- [Godot Steering AI Framework](#godot-steering-ai-framework)
+ - [Getting the framework](#getting-the-framework)
+ - [Introduction](#introduction)
+ - [The framework](#the-framework)
+ - [How it works](#how-it-works)
+ - [Documentation](#documentation)
+ - [Contributing](#contributing)
+ - [Support us](#support-us)
+ - [Join the community](#join-the-community)
+
+## Getting the framework
+
+This repository contains the framework and some demos for learning purposes.
+
+If you want just the framework with nothing else to get in the way or to create a Git submodule, use the [Godot Steering AI Framework Submodules](https://github.com/GDQuest/godot-steering-ai-framework-submodule) repository.
+
+## 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 available here: [Godot steering AI framework documentation](https://gdquest.gitbook.io/godot-3-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)
diff --git a/steering_ai/assets/icon.png b/steering_ai/assets/icon.png
new file mode 100644
index 0000000..ed8f05d
Binary files /dev/null and b/steering_ai/assets/icon.png differ
diff --git a/steering_ai/assets/icon.png.import b/steering_ai/assets/icon.png.import
new file mode 100644
index 0000000..19c032f
--- /dev/null
+++ b/steering_ai/assets/icon.png.import
@@ -0,0 +1,35 @@
+[remap]
+
+importer="texture"
+type="StreamTexture"
+path="res://.import/icon.png-b6a7fb2db36edd3d95dc42f1dc8c1c5d.stex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://assets/icon.png"
+dest_files=[ "res://.import/icon.png-b6a7fb2db36edd3d95dc42f1dc8c1c5d.stex" ]
+
+[params]
+
+compress/mode=0
+compress/lossy_quality=0.7
+compress/hdr_mode=0
+compress/bptc_ldr=0
+compress/normal_map=0
+flags/repeat=0
+flags/filter=true
+flags/mipmaps=false
+flags/anisotropic=false
+flags/srgb=2
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/HDR_as_SRGB=false
+process/invert_color=false
+process/normal_map_invert_y=false
+stream=false
+size_limit=0
+detect_3d=true
+svg/scale=1.0
diff --git a/steering_ai/assets/icon.svg b/steering_ai/assets/icon.svg
new file mode 100644
index 0000000..b690016
--- /dev/null
+++ b/steering_ai/assets/icon.svg
@@ -0,0 +1,151 @@
+
+
diff --git a/steering_ai/assets/icon.svg.import b/steering_ai/assets/icon.svg.import
new file mode 100644
index 0000000..3c98329
--- /dev/null
+++ b/steering_ai/assets/icon.svg.import
@@ -0,0 +1,35 @@
+[remap]
+
+importer="texture"
+type="StreamTexture"
+path="res://.import/icon.svg-56083ea2a1f1a4f1e49773bdc6d7826c.stex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://assets/icon.svg"
+dest_files=[ "res://.import/icon.svg-56083ea2a1f1a4f1e49773bdc6d7826c.stex" ]
+
+[params]
+
+compress/mode=0
+compress/lossy_quality=0.7
+compress/hdr_mode=0
+compress/bptc_ldr=0
+compress/normal_map=0
+flags/repeat=0
+flags/filter=true
+flags/mipmaps=false
+flags/anisotropic=false
+flags/srgb=2
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/HDR_as_SRGB=false
+process/invert_color=false
+process/normal_map_invert_y=false
+stream=false
+size_limit=0
+detect_3d=true
+svg/scale=1.0
diff --git a/steering_ai/assets/sprites/background.png b/steering_ai/assets/sprites/background.png
new file mode 100644
index 0000000..ad7e8ba
Binary files /dev/null and b/steering_ai/assets/sprites/background.png differ
diff --git a/steering_ai/assets/sprites/background.png.import b/steering_ai/assets/sprites/background.png.import
new file mode 100644
index 0000000..1835864
--- /dev/null
+++ b/steering_ai/assets/sprites/background.png.import
@@ -0,0 +1,35 @@
+[remap]
+
+importer="texture"
+type="StreamTexture"
+path="res://.import/background.png-dde469fb1f19281f3784b52d4bea96cd.stex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://assets/sprites/background.png"
+dest_files=[ "res://.import/background.png-dde469fb1f19281f3784b52d4bea96cd.stex" ]
+
+[params]
+
+compress/mode=0
+compress/lossy_quality=0.7
+compress/hdr_mode=0
+compress/bptc_ldr=0
+compress/normal_map=0
+flags/repeat=0
+flags/filter=true
+flags/mipmaps=false
+flags/anisotropic=false
+flags/srgb=2
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/HDR_as_SRGB=false
+process/invert_color=false
+process/normal_map_invert_y=false
+stream=false
+size_limit=0
+detect_3d=true
+svg/scale=1.0
diff --git a/steering_ai/assets/theme/button/disabled.stylebox b/steering_ai/assets/theme/button/disabled.stylebox
new file mode 100644
index 0000000..252f924
Binary files /dev/null and b/steering_ai/assets/theme/button/disabled.stylebox differ
diff --git a/steering_ai/assets/theme/button/focused.stylebox b/steering_ai/assets/theme/button/focused.stylebox
new file mode 100644
index 0000000..04863c6
Binary files /dev/null and b/steering_ai/assets/theme/button/focused.stylebox differ
diff --git a/steering_ai/assets/theme/button/hover.stylebox b/steering_ai/assets/theme/button/hover.stylebox
new file mode 100644
index 0000000..08fb085
Binary files /dev/null and b/steering_ai/assets/theme/button/hover.stylebox differ
diff --git a/steering_ai/assets/theme/button/normal.stylebox b/steering_ai/assets/theme/button/normal.stylebox
new file mode 100644
index 0000000..999edcf
Binary files /dev/null and b/steering_ai/assets/theme/button/normal.stylebox differ
diff --git a/steering_ai/assets/theme/button/pressed.stylebox b/steering_ai/assets/theme/button/pressed.stylebox
new file mode 100644
index 0000000..7cb801b
Binary files /dev/null and b/steering_ai/assets/theme/button/pressed.stylebox differ
diff --git a/steering_ai/assets/theme/empty.stylebox b/steering_ai/assets/theme/empty.stylebox
new file mode 100644
index 0000000..4467232
Binary files /dev/null and b/steering_ai/assets/theme/empty.stylebox differ
diff --git a/steering_ai/assets/theme/fonts/default_font.tres b/steering_ai/assets/theme/fonts/default_font.tres
new file mode 100644
index 0000000..7ca6e3a
--- /dev/null
+++ b/steering_ai/assets/theme/fonts/default_font.tres
@@ -0,0 +1,8 @@
+[gd_resource type="DynamicFont" load_steps=2 format=2]
+
+[ext_resource path="res://assets/theme/fonts/montserrat/Montserrat-Medium.ttf" type="DynamicFontData" id=1]
+
+[resource]
+size = 26
+use_filter = true
+font_data = ExtResource( 1 )
diff --git a/steering_ai/assets/theme/fonts/default_font_bold.tres b/steering_ai/assets/theme/fonts/default_font_bold.tres
new file mode 100644
index 0000000..92dcae0
--- /dev/null
+++ b/steering_ai/assets/theme/fonts/default_font_bold.tres
@@ -0,0 +1,8 @@
+[gd_resource type="DynamicFont" load_steps=2 format=2]
+
+[ext_resource path="res://assets/theme/fonts/montserrat/Montserrat-Bold.ttf" type="DynamicFontData" id=1]
+
+[resource]
+size = 26
+use_filter = true
+font_data = ExtResource( 1 )
diff --git a/steering_ai/assets/theme/fonts/default_font_code.tres b/steering_ai/assets/theme/fonts/default_font_code.tres
new file mode 100644
index 0000000..0138558
--- /dev/null
+++ b/steering_ai/assets/theme/fonts/default_font_code.tres
@@ -0,0 +1,8 @@
+[gd_resource type="DynamicFont" load_steps=2 format=2]
+
+[ext_resource path="res://assets/theme/fonts/source_code_pro/SourceCodePro-Medium.otf" type="DynamicFontData" id=1]
+
+[resource]
+size = 26
+use_filter = true
+font_data = ExtResource( 1 )
diff --git a/steering_ai/assets/theme/fonts/font_title.tres b/steering_ai/assets/theme/fonts/font_title.tres
new file mode 100644
index 0000000..a1788d3
--- /dev/null
+++ b/steering_ai/assets/theme/fonts/font_title.tres
@@ -0,0 +1,8 @@
+[gd_resource type="DynamicFont" load_steps=2 format=2]
+
+[ext_resource path="res://assets/theme/fonts/montserrat/Montserrat-Black.ttf" type="DynamicFontData" id=1]
+
+[resource]
+size = 34
+use_filter = true
+font_data = ExtResource( 1 )
diff --git a/steering_ai/assets/theme/fonts/montserrat/Montserrat-Black.ttf b/steering_ai/assets/theme/fonts/montserrat/Montserrat-Black.ttf
new file mode 100644
index 0000000..f0d24ad
Binary files /dev/null and b/steering_ai/assets/theme/fonts/montserrat/Montserrat-Black.ttf differ
diff --git a/steering_ai/assets/theme/fonts/montserrat/Montserrat-Bold.ttf b/steering_ai/assets/theme/fonts/montserrat/Montserrat-Bold.ttf
new file mode 100644
index 0000000..9a425b9
Binary files /dev/null and b/steering_ai/assets/theme/fonts/montserrat/Montserrat-Bold.ttf differ
diff --git a/steering_ai/assets/theme/fonts/montserrat/Montserrat-Medium.ttf b/steering_ai/assets/theme/fonts/montserrat/Montserrat-Medium.ttf
new file mode 100644
index 0000000..db5b1af
Binary files /dev/null and b/steering_ai/assets/theme/fonts/montserrat/Montserrat-Medium.ttf differ
diff --git a/steering_ai/assets/theme/fonts/source_code_pro/SourceCodePro-Medium.otf b/steering_ai/assets/theme/fonts/source_code_pro/SourceCodePro-Medium.otf
new file mode 100644
index 0000000..1b42738
Binary files /dev/null and b/steering_ai/assets/theme/fonts/source_code_pro/SourceCodePro-Medium.otf differ
diff --git a/steering_ai/assets/theme/gdquest.theme b/steering_ai/assets/theme/gdquest.theme
new file mode 100644
index 0000000..61b83dd
Binary files /dev/null and b/steering_ai/assets/theme/gdquest.theme differ
diff --git a/steering_ai/assets/theme/icons/chevron-right.svg b/steering_ai/assets/theme/icons/chevron-right.svg
new file mode 100644
index 0000000..258de41
--- /dev/null
+++ b/steering_ai/assets/theme/icons/chevron-right.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/steering_ai/assets/theme/icons/chevron-right.svg.import b/steering_ai/assets/theme/icons/chevron-right.svg.import
new file mode 100644
index 0000000..a70eff6
--- /dev/null
+++ b/steering_ai/assets/theme/icons/chevron-right.svg.import
@@ -0,0 +1,35 @@
+[remap]
+
+importer="texture"
+type="StreamTexture"
+path="res://.import/chevron-right.svg-f77dee7a088177a2ac1d467f4c7cd3e1.stex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://assets/theme/icons/chevron-right.svg"
+dest_files=[ "res://.import/chevron-right.svg-f77dee7a088177a2ac1d467f4c7cd3e1.stex" ]
+
+[params]
+
+compress/mode=0
+compress/lossy_quality=0.7
+compress/hdr_mode=0
+compress/bptc_ldr=0
+compress/normal_map=0
+flags/repeat=0
+flags/filter=true
+flags/mipmaps=false
+flags/anisotropic=false
+flags/srgb=2
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/HDR_as_SRGB=false
+process/invert_color=false
+process/normal_map_invert_y=false
+stream=false
+size_limit=0
+detect_3d=true
+svg/scale=1.0
diff --git a/steering_ai/assets/theme/icons/chevron-up.svg b/steering_ai/assets/theme/icons/chevron-up.svg
new file mode 100644
index 0000000..4eb5ecc
--- /dev/null
+++ b/steering_ai/assets/theme/icons/chevron-up.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/steering_ai/assets/theme/icons/chevron-up.svg.import b/steering_ai/assets/theme/icons/chevron-up.svg.import
new file mode 100644
index 0000000..79aeba4
--- /dev/null
+++ b/steering_ai/assets/theme/icons/chevron-up.svg.import
@@ -0,0 +1,35 @@
+[remap]
+
+importer="texture"
+type="StreamTexture"
+path="res://.import/chevron-up.svg-48b5b69265734774d0a7516dcc6f0863.stex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://assets/theme/icons/chevron-up.svg"
+dest_files=[ "res://.import/chevron-up.svg-48b5b69265734774d0a7516dcc6f0863.stex" ]
+
+[params]
+
+compress/mode=0
+compress/lossy_quality=0.7
+compress/hdr_mode=0
+compress/bptc_ldr=0
+compress/normal_map=0
+flags/repeat=0
+flags/filter=true
+flags/mipmaps=false
+flags/anisotropic=false
+flags/srgb=2
+process/fix_alpha_border=true
+process/premult_alpha=false
+process/HDR_as_SRGB=false
+process/invert_color=false
+process/normal_map_invert_y=false
+stream=false
+size_limit=0
+detect_3d=true
+svg/scale=1.0
diff --git a/steering_ai/assets/theme/panel/panel.stylebox b/steering_ai/assets/theme/panel/panel.stylebox
new file mode 100644
index 0000000..0c557d0
Binary files /dev/null and b/steering_ai/assets/theme/panel/panel.stylebox differ
diff --git a/steering_ai/assets/theme/separator/line.tres b/steering_ai/assets/theme/separator/line.tres
new file mode 100644
index 0000000..49130dc
--- /dev/null
+++ b/steering_ai/assets/theme/separator/line.tres
@@ -0,0 +1,5 @@
+[gd_resource type="StyleBoxLine" format=2]
+
+[resource]
+color = Color( 1, 1, 1, 0.196078 )
+thickness = 2
diff --git a/steering_ai/assets/theme/slider/grabber_area.stylebox b/steering_ai/assets/theme/slider/grabber_area.stylebox
new file mode 100644
index 0000000..5fd31a9
Binary files /dev/null and b/steering_ai/assets/theme/slider/grabber_area.stylebox differ
diff --git a/steering_ai/assets/theme/slider/slider.stylebox b/steering_ai/assets/theme/slider/slider.stylebox
new file mode 100644
index 0000000..f89f29b
Binary files /dev/null and b/steering_ai/assets/theme/slider/slider.stylebox differ
diff --git a/steering_ai/default_env.tres b/steering_ai/default_env.tres
new file mode 100644
index 0000000..4f08e8f
--- /dev/null
+++ b/steering_ai/default_env.tres
@@ -0,0 +1,7 @@
+[gd_resource type="Environment3D" load_steps=2 format=2]
+
+[sub_resource type="ProceduralSky" id=1]
+
+[resource]
+background_mode = 2
+background_sky = SubResource( 1 )
diff --git a/steering_ai/project.pandemonium b/steering_ai/project.pandemonium
new file mode 100644
index 0000000..c91957d
--- /dev/null
+++ b/steering_ai/project.pandemonium
@@ -0,0 +1,76 @@
+; Engine configuration file.
+; It's best edited using the editor UI and not directly,
+; since the parameters that go here are not all obvious.
+;
+; Format:
+; [section] ; section goes between []
+; param=value ; assign values to parameters
+
+config_version=4
+
+_global_script_classes=[ {
+"base": "Control",
+"class": @"DemoPickerUI",
+"language": @"GDScript",
+"path": "res://Demos/DemoPickerUI.gd"
+} ]
+_global_script_class_icons={
+@"DemoPickerUI": ""
+}
+
+[application]
+
+config/name="Godot Steering AI Framework Demos"
+run/main_scene="res://Demos/DemoSelector.tscn"
+config/icon="res://assets/icon.png"
+
+[display]
+
+window/size/width=1920
+window/size/height=1080
+window/size/always_on_top=true
+window/size/test_width=1280
+window/size/test_height=720
+window/stretch/mode="2d"
+window/stretch/aspect="expand"
+
+[editor_plugins]
+
+enabled=PoolStringArray( )
+
+[input]
+
+sf_left={
+"deadzone": 0.5,
+"events": [ Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"alt":false,"shift":false,"control":false,"meta":false,"command":false,"pressed":false,"scancode":65,"physical_scancode":0,"unicode":0,"echo":false,"script":null)
+, Object(InputEventJoypadMotion,"resource_local_to_scene":false,"resource_name":"","device":0,"axis":0,"axis_value":-1.0,"script":null)
+ ]
+}
+sf_right={
+"deadzone": 0.5,
+"events": [ Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"alt":false,"shift":false,"control":false,"meta":false,"command":false,"pressed":false,"scancode":68,"physical_scancode":0,"unicode":0,"echo":false,"script":null)
+, Object(InputEventJoypadMotion,"resource_local_to_scene":false,"resource_name":"","device":0,"axis":0,"axis_value":1.0,"script":null)
+ ]
+}
+sf_up={
+"deadzone": 0.5,
+"events": [ Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"alt":false,"shift":false,"control":false,"meta":false,"command":false,"pressed":false,"scancode":87,"physical_scancode":0,"unicode":0,"echo":false,"script":null)
+, Object(InputEventJoypadMotion,"resource_local_to_scene":false,"resource_name":"","device":0,"axis":1,"axis_value":-1.0,"script":null)
+ ]
+}
+sf_down={
+"deadzone": 0.5,
+"events": [ Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"alt":false,"shift":false,"control":false,"meta":false,"command":false,"pressed":false,"scancode":83,"physical_scancode":0,"unicode":0,"echo":false,"script":null)
+, Object(InputEventJoypadMotion,"resource_local_to_scene":false,"resource_name":"","device":0,"axis":1,"axis_value":1.0,"script":null)
+ ]
+}
+toggle_fullscreen={
+"deadzone": 0.5,
+"events": [ Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"alt":false,"shift":false,"control":false,"meta":false,"command":false,"pressed":false,"scancode":16777254,"physical_scancode":0,"unicode":0,"echo":false,"script":null)
+ ]
+}
+click={
+"deadzone": 0.5,
+"events": [ Object(InputEventMouseButton,"resource_local_to_scene":false,"resource_name":"","device":0,"alt":false,"shift":false,"control":false,"meta":false,"command":false,"button_mask":0,"position":Vector2( 0, 0 ),"global_position":Vector2( 0, 0 ),"factor":1.0,"button_index":1,"pressed":false,"doubleclick":false,"script":null)
+ ]
+}