diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..56594a2 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.import/ \ No newline at end of file diff --git a/mvp.md b/docs/mvp.md similarity index 90% rename from mvp.md rename to docs/mvp.md index 2e738ff..059d985 100644 --- a/mvp.md +++ b/docs/mvp.md @@ -8,6 +8,7 @@ - Arrive - Pursue/Evade - Face +- LookWhereYouGo - FollowPath - Separation - AvoidCollisions diff --git a/techdetails.md b/docs/techdetails.md similarity index 100% rename from techdetails.md rename to docs/techdetails.md diff --git a/project/assets/sprites/large_circle.png b/project/assets/sprites/large_circle.png new file mode 100644 index 0000000..e69cc11 Binary files /dev/null and b/project/assets/sprites/large_circle.png differ diff --git a/project/assets/sprites/large_circle.png.import b/project/assets/sprites/large_circle.png.import new file mode 100644 index 0000000..7671c6c --- /dev/null +++ b/project/assets/sprites/large_circle.png.import @@ -0,0 +1,34 @@ +[remap] + +importer="texture" +type="StreamTexture" +path="res://.import/large_circle.png-31c2e25548cad683b9cdbdea4df32e13.stex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/sprites/large_circle.png" +dest_files=[ "res://.import/large_circle.png-31c2e25548cad683b9cdbdea4df32e13.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 +stream=false +size_limit=0 +detect_3d=true +svg/scale=1.0 diff --git a/project/assets/sprites/small_circle.png b/project/assets/sprites/small_circle.png new file mode 100644 index 0000000..0676b8f Binary files /dev/null and b/project/assets/sprites/small_circle.png differ diff --git a/project/assets/sprites/small_circle.png.import b/project/assets/sprites/small_circle.png.import new file mode 100644 index 0000000..560c875 --- /dev/null +++ b/project/assets/sprites/small_circle.png.import @@ -0,0 +1,34 @@ +[remap] + +importer="texture" +type="StreamTexture" +path="res://.import/small_circle.png-e9ef462acf0465fde3767e7b0877ff44.stex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/sprites/small_circle.png" +dest_files=[ "res://.import/small_circle.png-e9ef462acf0465fde3767e7b0877ff44.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 +stream=false +size_limit=0 +detect_3d=true +svg/scale=1.0 diff --git a/project/assets/sprites/triangle.png b/project/assets/sprites/triangle.png new file mode 100644 index 0000000..5d8b026 Binary files /dev/null and b/project/assets/sprites/triangle.png differ diff --git a/project/assets/sprites/triangle.png.import b/project/assets/sprites/triangle.png.import new file mode 100644 index 0000000..e63a133 --- /dev/null +++ b/project/assets/sprites/triangle.png.import @@ -0,0 +1,34 @@ +[remap] + +importer="texture" +type="StreamTexture" +path="res://.import/triangle.png-fa05d9e46946b626c9973edf66af1138.stex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/sprites/triangle.png" +dest_files=[ "res://.import/triangle.png-fa05d9e46946b626c9973edf66af1138.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 +stream=false +size_limit=0 +detect_3d=true +svg/scale=1.0 diff --git a/project/assets/theme/button/disabled.stylebox b/project/assets/theme/button/disabled.stylebox new file mode 100644 index 0000000..5838b76 Binary files /dev/null and b/project/assets/theme/button/disabled.stylebox differ diff --git a/project/assets/theme/button/focused.stylebox b/project/assets/theme/button/focused.stylebox new file mode 100644 index 0000000..1b8fd76 Binary files /dev/null and b/project/assets/theme/button/focused.stylebox differ diff --git a/project/assets/theme/button/hover.stylebox b/project/assets/theme/button/hover.stylebox new file mode 100644 index 0000000..fb464a1 Binary files /dev/null and b/project/assets/theme/button/hover.stylebox differ diff --git a/project/assets/theme/button/normal.stylebox b/project/assets/theme/button/normal.stylebox new file mode 100644 index 0000000..4f8c7da Binary files /dev/null and b/project/assets/theme/button/normal.stylebox differ diff --git a/project/assets/theme/button/pressed.stylebox b/project/assets/theme/button/pressed.stylebox new file mode 100644 index 0000000..c04a001 Binary files /dev/null and b/project/assets/theme/button/pressed.stylebox differ diff --git a/project/assets/theme/empty.stylebox b/project/assets/theme/empty.stylebox new file mode 100644 index 0000000..4467232 Binary files /dev/null and b/project/assets/theme/empty.stylebox differ diff --git a/project/assets/theme/fonts/default_font.tres b/project/assets/theme/fonts/default_font.tres new file mode 100644 index 0000000..9f845d7 --- /dev/null +++ b/project/assets/theme/fonts/default_font.tres @@ -0,0 +1,11 @@ +[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 = 20 +use_filter = true +font_data = ExtResource( 1 ) + diff --git a/project/assets/theme/fonts/default_font_bold.tres b/project/assets/theme/fonts/default_font_bold.tres new file mode 100644 index 0000000..238d067 --- /dev/null +++ b/project/assets/theme/fonts/default_font_bold.tres @@ -0,0 +1,11 @@ +[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 = 20 +use_filter = true +font_data = ExtResource( 1 ) + diff --git a/project/assets/theme/fonts/default_font_code.tres b/project/assets/theme/fonts/default_font_code.tres new file mode 100644 index 0000000..4f13506 --- /dev/null +++ b/project/assets/theme/fonts/default_font_code.tres @@ -0,0 +1,11 @@ +[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 = 20 +use_filter = true +font_data = ExtResource( 1 ) + diff --git a/project/assets/theme/fonts/font_title.tres b/project/assets/theme/fonts/font_title.tres new file mode 100644 index 0000000..f85490d --- /dev/null +++ b/project/assets/theme/fonts/font_title.tres @@ -0,0 +1,11 @@ +[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 = 28 +use_filter = true +font_data = ExtResource( 1 ) + diff --git a/project/assets/theme/fonts/montserrat/Montserrat-Black.ttf b/project/assets/theme/fonts/montserrat/Montserrat-Black.ttf new file mode 100644 index 0000000..f0d24ad Binary files /dev/null and b/project/assets/theme/fonts/montserrat/Montserrat-Black.ttf differ diff --git a/project/assets/theme/fonts/montserrat/Montserrat-Bold.ttf b/project/assets/theme/fonts/montserrat/Montserrat-Bold.ttf new file mode 100644 index 0000000..9a425b9 Binary files /dev/null and b/project/assets/theme/fonts/montserrat/Montserrat-Bold.ttf differ diff --git a/project/assets/theme/fonts/montserrat/Montserrat-Medium.ttf b/project/assets/theme/fonts/montserrat/Montserrat-Medium.ttf new file mode 100644 index 0000000..db5b1af Binary files /dev/null and b/project/assets/theme/fonts/montserrat/Montserrat-Medium.ttf differ diff --git a/project/assets/theme/fonts/source_code_pro/SourceCodePro-Medium.otf b/project/assets/theme/fonts/source_code_pro/SourceCodePro-Medium.otf new file mode 100644 index 0000000..1b42738 Binary files /dev/null and b/project/assets/theme/fonts/source_code_pro/SourceCodePro-Medium.otf differ diff --git a/project/assets/theme/gdquest.theme b/project/assets/theme/gdquest.theme new file mode 100644 index 0000000..1cc33cd Binary files /dev/null and b/project/assets/theme/gdquest.theme differ diff --git a/project/assets/theme/icons/chevron-right.svg b/project/assets/theme/icons/chevron-right.svg new file mode 100644 index 0000000..258de41 --- /dev/null +++ b/project/assets/theme/icons/chevron-right.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/project/assets/theme/icons/chevron-right.svg.import b/project/assets/theme/icons/chevron-right.svg.import new file mode 100644 index 0000000..000f669 --- /dev/null +++ b/project/assets/theme/icons/chevron-right.svg.import @@ -0,0 +1,34 @@ +[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 +stream=false +size_limit=0 +detect_3d=true +svg/scale=1.0 diff --git a/project/assets/theme/icons/chevron-up.svg b/project/assets/theme/icons/chevron-up.svg new file mode 100644 index 0000000..4eb5ecc --- /dev/null +++ b/project/assets/theme/icons/chevron-up.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/project/assets/theme/icons/chevron-up.svg.import b/project/assets/theme/icons/chevron-up.svg.import new file mode 100644 index 0000000..14e96f7 --- /dev/null +++ b/project/assets/theme/icons/chevron-up.svg.import @@ -0,0 +1,34 @@ +[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 +stream=false +size_limit=0 +detect_3d=true +svg/scale=1.0 diff --git a/project/assets/theme/panel/panel.stylebox b/project/assets/theme/panel/panel.stylebox new file mode 100644 index 0000000..1c623a7 Binary files /dev/null and b/project/assets/theme/panel/panel.stylebox differ diff --git a/project/assets/theme/separator/line.tres b/project/assets/theme/separator/line.tres new file mode 100644 index 0000000..85da689 --- /dev/null +++ b/project/assets/theme/separator/line.tres @@ -0,0 +1,7 @@ +[gd_resource type="StyleBoxLine" format=2] + +[resource] + +color = Color( 1, 1, 1, 0.196078 ) +thickness = 2 + diff --git a/project/assets/theme/slider/grabber_area.stylebox b/project/assets/theme/slider/grabber_area.stylebox new file mode 100644 index 0000000..2f566a6 Binary files /dev/null and b/project/assets/theme/slider/grabber_area.stylebox differ diff --git a/project/assets/theme/slider/slider.stylebox b/project/assets/theme/slider/slider.stylebox new file mode 100644 index 0000000..6fbfe55 Binary files /dev/null and b/project/assets/theme/slider/slider.stylebox differ diff --git a/project/default_env.tres b/project/default_env.tres new file mode 100644 index 0000000..20207a4 --- /dev/null +++ b/project/default_env.tres @@ -0,0 +1,7 @@ +[gd_resource type="Environment" load_steps=2 format=2] + +[sub_resource type="ProceduralSky" id=1] + +[resource] +background_mode = 2 +background_sky = SubResource( 1 ) diff --git a/project/demos/arrive/ArriveDemo.gd b/project/demos/arrive/ArriveDemo.gd new file mode 100644 index 0000000..3df302c --- /dev/null +++ b/project/demos/arrive/ArriveDemo.gd @@ -0,0 +1,37 @@ +extends Node2D + + +onready var target: = $Target +onready var arriver: = $Arriver +onready var gui: = $GUI + + +func _ready() -> void: + gui.connect("align_tolerance_changed", self, "_on_GUI_align_tolerance_changed") + gui.connect("decel_radius_changed", self, "_on_GUI_decel_radius_changed") + gui.connect("max_speed_changed", self, "_on_GUI_max_speed_changed") + gui.connect("max_accel_changed", self, "_on_GUI_max_accel_changed") + gui.max_speed.text = str(arriver._agent.max_linear_speed) + gui.max_accel.text = str(arriver._agent.max_linear_acceleration) + gui.arrival_tolerance.text = str(arriver._arrive.arrival_tolerance) + gui.deceleration_radius.text = str(arriver._arrive.deceleration_radius) + + +func draw(location: Vector2) -> void: + target.draw(location) + + +func _on_GUI_align_tolerance_changed(value: int) -> void: + arriver._arrive.arrival_tolerance = value + + +func _on_GUI_decel_radius_changed(value: int) -> void: + arriver._arrive.deceleration_radius = value + + +func _on_GUI_max_speed_changed(value: int) -> void: + arriver._agent.max_linear_speed = value + + +func _on_GUI_max_accel_changed(value: int) -> void: + arriver._agent.max_linear_acceleration = value diff --git a/project/demos/arrive/ArriveDemo.tscn b/project/demos/arrive/ArriveDemo.tscn new file mode 100644 index 0000000..23a18f5 --- /dev/null +++ b/project/demos/arrive/ArriveDemo.tscn @@ -0,0 +1,25 @@ +[gd_scene load_steps=6 format=2] + +[ext_resource path="res://demos/arrive/Arriver.gd" type="Script" id=1] +[ext_resource path="res://demos/arrive/ArriveDemo.gd" type="Script" id=2] +[ext_resource path="res://demos/arrive/Target.gd" type="Script" id=3] +[ext_resource path="res://demos/arrive/GUI.tscn" type="PackedScene" id=4] + +[sub_resource type="CircleShape2D" id=1] +radius = 15.0 + +[node name="ArriveDemo" type="Node2D"] +script = ExtResource( 2 ) + +[node name="Arriver" type="KinematicBody2D" parent="."] +script = ExtResource( 1 ) + +[node name="CollisionShape2D" type="CollisionShape2D" parent="Arriver"] +shape = SubResource( 1 ) + +[node name="Target" type="Node2D" parent="."] +script = ExtResource( 3 ) + +[node name="GUI" parent="." instance=ExtResource( 4 )] +margin_right = 227.0 +margin_bottom = 184.0 diff --git a/project/demos/arrive/Arriver.gd b/project/demos/arrive/Arriver.gd new file mode 100644 index 0000000..f582263 --- /dev/null +++ b/project/demos/arrive/Arriver.gd @@ -0,0 +1,46 @@ +extends KinematicBody2D + + +onready var collision_shape: = $CollisionShape2D + +var _radius: = 0.0 +var _agent: = GSTSteeringAgent.new() +var _target: = GSTAgentLocation.new() +var _arrive: = GSTArrive.new(_agent, _target) +var _accel: = GSTTargetAcceleration.new() + +var _velocity: = Vector2() +var _drag: = 1.0 + + +func _ready() -> void: + _radius = collision_shape.shape.radius + _agent.max_linear_acceleration = 10 + _agent.max_linear_speed = 200 + _arrive.arrival_tolerance = 25 + _arrive.deceleration_radius = 225 + + +func _draw() -> void: + draw_circle(Vector2.ZERO, _radius, Color.red) + + +func _unhandled_input(event: InputEvent) -> void: + if event is InputEventMouseButton: + var mb: InputEventMouseButton = event + if mb.button_index == BUTTON_LEFT and mb.pressed: + _target.position = Vector3(mb.position.x, mb.position.y, 0) + owner.draw(Vector2(_target.position.x, _target.position.y)) + + +func _physics_process(delta: float) -> void: + _accel = _arrive.calculate_steering(_accel) + _velocity += Vector2(_accel.linear.x, _accel.linear.y) + _velocity -= _velocity * _drag * delta + _velocity = move_and_slide(_velocity) + _update_agent() + + +func _update_agent() -> void: + _agent.position = Vector3(global_position.x, global_position.y, 0) + _agent.linear_velocity = Vector3(_velocity.x, _velocity.y, 0) diff --git a/project/demos/arrive/GUI.gd b/project/demos/arrive/GUI.gd new file mode 100644 index 0000000..3ed6a03 --- /dev/null +++ b/project/demos/arrive/GUI.gd @@ -0,0 +1,47 @@ +extends MarginContainer + + +signal max_speed_changed(value) +signal max_accel_changed(value) +signal align_tolerance_changed(value) +signal decel_radius_changed(value) + +onready var max_speed: = $Controls/MaxSpeed/LineEdit +onready var max_accel: = $Controls/MaxAccel/LineEdit +onready var arrival_tolerance: = $Controls/ArrivalTolerance/LineEdit +onready var deceleration_radius: = $Controls/DecelRadius/LineEdit + + +func _ready() -> void: + max_speed.connect("text_changed", self, "_on_MaxSpeed_text_changed") + max_accel.connect("text_changed", self, "_on_MaxAccel_text_changed") + arrival_tolerance.connect("text_changed", self, "_on_ArrivalTolerance_text_changed") + deceleration_radius.connect("text_changed", self, "_on_DecelerationRadius_text_changed") + + +func _unhandled_input(event: InputEvent) -> void: + if event is InputEventMouseButton: + max_speed.release_focus() + max_accel.release_focus() + arrival_tolerance.release_focus() + deceleration_radius.release_focus() + + +func _on_MaxSpeed_text_changed(new_text: String) -> void: + if new_text.is_valid_integer(): + emit_signal("max_speed_changed", int(float(new_text))) + + +func _on_MaxAccel_text_changed(new_text: String) -> void: + if new_text.is_valid_integer(): + emit_signal("max_accel_changed", int(float(new_text))) + + +func _on_ArrivalTolerance_text_changed(new_text: String) -> void: + if new_text.is_valid_integer(): + emit_signal("align_tolerance_changed", int(float(new_text))) + + +func _on_DecelerationRadius_text_changed(new_text: String) -> void: + if new_text.is_valid_integer(): + emit_signal("decel_radius_changed", int(float(new_text))) diff --git a/project/demos/arrive/GUI.tscn b/project/demos/arrive/GUI.tscn new file mode 100644 index 0000000..d870e56 --- /dev/null +++ b/project/demos/arrive/GUI.tscn @@ -0,0 +1,108 @@ +[gd_scene load_steps=2 format=2] + +[ext_resource path="res://demos/arrive/GUI.gd" type="Script" id=1] + +[node name="GUI" type="MarginContainer"] +anchor_bottom = 1.0 +margin_right = 40.0 +custom_constants/margin_right = 20 +custom_constants/margin_top = 20 +custom_constants/margin_left = 20 +custom_constants/margin_bottom = 20 +script = ExtResource( 1 ) +__meta__ = { +"_edit_use_anchors_": false +} + +[node name="Controls" type="VBoxContainer" parent="."] +margin_left = 20.0 +margin_top = 20.0 +margin_right = 207.0 +margin_bottom = 580.0 + +[node name="MaxSpeed" type="HBoxContainer" parent="Controls"] +margin_right = 187.0 +margin_bottom = 24.0 + +[node name="Label" type="Label" parent="Controls/MaxSpeed"] +margin_top = 4.0 +margin_right = 125.0 +margin_bottom = 19.0 +rect_min_size = Vector2( 125, 15 ) +text = "Max speed" + +[node name="LineEdit" type="LineEdit" parent="Controls/MaxSpeed"] +margin_left = 129.0 +margin_right = 187.0 +margin_bottom = 24.0 +focus_mode = 1 + +[node name="MaxAccel" type="HBoxContainer" parent="Controls"] +margin_top = 28.0 +margin_right = 187.0 +margin_bottom = 52.0 + +[node name="Label" type="Label" parent="Controls/MaxAccel"] +margin_top = 4.0 +margin_right = 125.0 +margin_bottom = 19.0 +rect_min_size = Vector2( 125, 15 ) +text = "Max acceleration" + +[node name="LineEdit" type="LineEdit" parent="Controls/MaxAccel"] +margin_left = 129.0 +margin_right = 187.0 +margin_bottom = 24.0 +focus_mode = 1 + +[node name="ArrivalTolerance" type="HBoxContainer" parent="Controls"] +margin_top = 56.0 +margin_right = 187.0 +margin_bottom = 80.0 + +[node name="Label" type="Label" parent="Controls/ArrivalTolerance"] +margin_top = 4.0 +margin_right = 125.0 +margin_bottom = 19.0 +rect_min_size = Vector2( 125, 15 ) +text = "Arrival tolerance" + +[node name="LineEdit" type="LineEdit" parent="Controls/ArrivalTolerance"] +margin_left = 129.0 +margin_right = 187.0 +margin_bottom = 24.0 +focus_mode = 1 + +[node name="DecelRadius" type="HBoxContainer" parent="Controls"] +margin_top = 84.0 +margin_right = 187.0 +margin_bottom = 108.0 + +[node name="Label" type="Label" parent="Controls/DecelRadius"] +margin_top = 4.0 +margin_right = 125.0 +margin_bottom = 19.0 +rect_min_size = Vector2( 125, 15 ) +text = "Deceleration radius" + +[node name="LineEdit" type="LineEdit" parent="Controls/DecelRadius"] +margin_left = 129.0 +margin_right = 187.0 +margin_bottom = 24.0 +focus_mode = 1 + +[node name="Help" type="VBoxContainer" parent="Controls"] +margin_top = 112.0 +margin_right = 187.0 +margin_bottom = 144.0 + +[node name="Controls" type="Label" parent="Controls/Help"] +margin_right = 187.0 +margin_bottom = 14.0 +text = "Controls" + +[node name="Label" type="Label" parent="Controls/Help"] +margin_top = 18.0 +margin_right = 187.0 +margin_bottom = 32.0 +text = "Mouse click" diff --git a/project/demos/arrive/Target.gd b/project/demos/arrive/Target.gd new file mode 100644 index 0000000..831df6c --- /dev/null +++ b/project/demos/arrive/Target.gd @@ -0,0 +1,14 @@ +extends Node2D + + +var target: = Vector2.ZERO + + +func _draw() -> void: + draw_circle(target, 20, Color(1, 1, 0, 0.25)) + draw_circle(target, 5, Color.yellow) + + +func draw(location: Vector2) -> void: + target = location + update() diff --git a/project/demos/face/FaceDemo.gd b/project/demos/face/FaceDemo.gd new file mode 100644 index 0000000..df51ca2 --- /dev/null +++ b/project/demos/face/FaceDemo.gd @@ -0,0 +1,34 @@ +extends Node2D + + +onready var player: = $Player +onready var gui: = $GUI +onready var turret: = $Turret + + +func _ready() -> void: + gui.connect("align_tolerance_changed", self, "_on_GUI_align_tolerance_changed") + gui.connect("decel_radius_changed", self, "_on_GUI_decel_radius_changed") + gui.connect("max_accel_changed", self, "_on_GUI_max_accel_changed") + gui.connect("max_speed_changed", self, "_on_GUI_max_speed_changed") + turret.setup() + gui.align_tolerance.text = str(int(rad2deg(turret._face.alignment_tolerance))) + gui.decel_radius.text = str(int(rad2deg(turret._face.deceleration_radius))) + gui.max_speed.text = str(int(rad2deg(turret._agent.max_angular_speed))) + gui.max_accel.text = str(int(rad2deg(turret._agent.max_angular_acceleration))) + + +func _on_GUI_align_tolerance_changed(value: int) -> void: + turret._face.alignment_tolerance = deg2rad(value) + + +func _on_GUI_decel_radius_changed(value: int) -> void: + turret._face.deceleration_radius = deg2rad(value) + + +func _on_GUI_max_accel_changed(value: int) -> void: + turret._agent.max_angular_acceleration = deg2rad(value) + + +func _on_GUI_max_speed_changed(value: int) -> void: + turret._agent.max_angular_speed = deg2rad(value) diff --git a/project/demos/face/FaceDemo.tscn b/project/demos/face/FaceDemo.tscn new file mode 100644 index 0000000..8960aed --- /dev/null +++ b/project/demos/face/FaceDemo.tscn @@ -0,0 +1,36 @@ +[gd_scene load_steps=7 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/face/GUI.tscn" type="PackedScene" id=4] + +[sub_resource type="CircleShape2D" id=1] +radius = 20.0 + +[sub_resource type="CircleShape2D" id=2] +radius = 30.0 + +[node name="FaceDemo" type="Node2D"] +script = ExtResource( 2 ) +__meta__ = { +"_editor_description_": "A demo showing the usage of the Face steering behavior." +} + +[node name="Player" type="KinematicBody2D" parent="."] +position = Vector2( 512, 450 ) +script = ExtResource( 3 ) + +[node name="CollisionShape2D" type="CollisionShape2D" parent="Player"] +shape = SubResource( 1 ) + +[node name="Turret" type="KinematicBody2D" parent="."] +position = Vector2( 512, 150 ) +script = ExtResource( 1 ) + +[node name="CollisionShape2D" type="CollisionShape2D" parent="Turret"] +shape = SubResource( 2 ) + +[node name="GUI" parent="." instance=ExtResource( 4 )] +margin_right = 232.0 +margin_bottom = 204.0 diff --git a/project/demos/face/GUI.gd b/project/demos/face/GUI.gd new file mode 100644 index 0000000..4284c41 --- /dev/null +++ b/project/demos/face/GUI.gd @@ -0,0 +1,47 @@ +extends MarginContainer + + +signal max_speed_changed(value) +signal max_accel_changed(value) +signal align_tolerance_changed(value) +signal decel_radius_changed(value) + +onready var max_speed: = $Controls/MaxSpeed/LineEdit +onready var max_accel: = $Controls/MaxAccel/LineEdit +onready var align_tolerance: = $Controls/AlignmentTolerance/LineEdit +onready var decel_radius: = $Controls/DecelerationRadius/LineEdit + + +func _ready() -> void: + max_speed.connect("text_changed", self, "_on_MaxSpeed_text_changed") + max_accel.connect("text_changed", self, "_on_MaxAccel_text_changed") + align_tolerance.connect("text_changed", self, "_on_AlignTolerance_text_changed") + decel_radius.connect("text_changed", self, "_on_DecelRadius_text_changed") + + +func _unhandled_input(event: InputEvent) -> void: + if event is InputEventMouseButton: + max_speed.release_focus() + max_accel.release_focus() + align_tolerance.release_focus() + decel_radius.release_focus() + + +func _on_MaxSpeed_text_changed(new_text: String) -> void: + if new_text.is_valid_integer(): + emit_signal("max_speed_changed", int(float(new_text))) + + +func _on_MaxAccel_text_changed(new_text: String) -> void: + if new_text.is_valid_integer(): + emit_signal("max_accel_changed", int(float(new_text))) + + +func _on_AlignTolerance_text_changed(new_text: String) -> void: + if new_text.is_valid_integer(): + emit_signal("align_tolerance_changed", int(float(new_text))) + + +func _on_DecelRadius_text_changed(new_text: String) -> void: + if new_text.is_valid_integer(): + emit_signal("decel_radius_changed", int(float(new_text))) diff --git a/project/demos/face/GUI.tscn b/project/demos/face/GUI.tscn new file mode 100644 index 0000000..09eef08 --- /dev/null +++ b/project/demos/face/GUI.tscn @@ -0,0 +1,153 @@ +[gd_scene load_steps=2 format=2] + +[ext_resource path="res://demos/face/GUI.gd" type="Script" id=1] + +[node name="GUI" type="MarginContainer"] +anchor_bottom = 1.0 +margin_right = 40.0 +custom_constants/margin_right = 20 +custom_constants/margin_top = 20 +custom_constants/margin_left = 20 +custom_constants/margin_bottom = 20 +script = ExtResource( 1 ) +__meta__ = { +"_edit_use_anchors_": false +} + +[node name="Controls" type="VBoxContainer" parent="."] +margin_left = 20.0 +margin_top = 20.0 +margin_right = 212.0 +margin_bottom = 580.0 + +[node name="MaxSpeed" type="HBoxContainer" parent="Controls"] +margin_right = 192.0 +margin_bottom = 24.0 + +[node name="Label" type="Label" parent="Controls/MaxSpeed"] +margin_top = 4.0 +margin_right = 130.0 +margin_bottom = 19.0 +rect_min_size = Vector2( 130, 15 ) +text = "Max speed" + +[node name="LineEdit" type="LineEdit" parent="Controls/MaxSpeed"] +margin_left = 134.0 +margin_right = 192.0 +margin_bottom = 24.0 +focus_mode = 1 + +[node name="MaxAccel" type="HBoxContainer" parent="Controls"] +margin_top = 28.0 +margin_right = 192.0 +margin_bottom = 52.0 + +[node name="Label" type="Label" parent="Controls/MaxAccel"] +margin_top = 4.0 +margin_right = 130.0 +margin_bottom = 19.0 +rect_min_size = Vector2( 130, 15 ) +text = "Max acceleration" + +[node name="LineEdit" type="LineEdit" parent="Controls/MaxAccel"] +margin_left = 134.0 +margin_right = 192.0 +margin_bottom = 24.0 +focus_mode = 1 + +[node name="AlignmentTolerance" type="HBoxContainer" parent="Controls"] +margin_top = 56.0 +margin_right = 192.0 +margin_bottom = 80.0 + +[node name="Label" type="Label" parent="Controls/AlignmentTolerance"] +margin_top = 4.0 +margin_right = 130.0 +margin_bottom = 19.0 +rect_min_size = Vector2( 130, 15 ) +text = "Alignment tolerance" + +[node name="LineEdit" type="LineEdit" parent="Controls/AlignmentTolerance"] +margin_left = 134.0 +margin_right = 192.0 +margin_bottom = 24.0 +focus_mode = 1 + +[node name="DecelerationRadius" type="HBoxContainer" parent="Controls"] +margin_top = 84.0 +margin_right = 192.0 +margin_bottom = 108.0 + +[node name="Label" type="Label" parent="Controls/DecelerationRadius"] +margin_top = 4.0 +margin_right = 130.0 +margin_bottom = 19.0 +rect_min_size = Vector2( 130, 15 ) +text = "Deceleration radius" + +[node name="LineEdit" type="LineEdit" parent="Controls/DecelerationRadius"] +margin_left = 134.0 +margin_right = 192.0 +margin_bottom = 24.0 +focus_mode = 1 + +[node name="Help" type="VBoxContainer" parent="Controls"] +margin_top = 112.0 +margin_right = 192.0 +margin_bottom = 164.0 + +[node name="Controls" type="Label" parent="Controls/Help"] +margin_right = 192.0 +margin_bottom = 14.0 +text = "Controls" + +[node name="GridContainer" type="GridContainer" parent="Controls/Help"] +margin_top = 18.0 +margin_right = 192.0 +margin_bottom = 52.0 +columns = 3 + +[node name="Sep" type="Control" parent="Controls/Help/GridContainer"] +margin_right = 15.0 +margin_bottom = 15.0 +rect_min_size = Vector2( 15, 15 ) + +[node name="W" type="Label" parent="Controls/Help/GridContainer"] +margin_left = 19.0 +margin_right = 34.0 +margin_bottom = 15.0 +rect_min_size = Vector2( 15, 15 ) +text = "W" +align = 1 + +[node name="Sep2" type="Control" parent="Controls/Help/GridContainer"] +margin_left = 38.0 +margin_right = 53.0 +margin_bottom = 15.0 +rect_min_size = Vector2( 15, 15 ) + +[node name="A" type="Label" parent="Controls/Help/GridContainer"] +margin_top = 19.0 +margin_right = 15.0 +margin_bottom = 34.0 +rect_min_size = Vector2( 15, 15 ) +text = "A" +align = 1 + +[node name="S" type="Label" parent="Controls/Help/GridContainer"] +margin_left = 19.0 +margin_top = 19.0 +margin_right = 34.0 +margin_bottom = 34.0 +rect_min_size = Vector2( 15, 15 ) +text = "S" +align = 1 + +[node name="D" type="Label" parent="Controls/Help/GridContainer"] +margin_left = 38.0 +margin_top = 19.0 +margin_right = 53.0 +margin_bottom = 34.0 +rect_min_size = Vector2( 15, 15 ) +text = "D" +align = 1 diff --git a/project/demos/face/Player.gd b/project/demos/face/Player.gd new file mode 100644 index 0000000..d5fa879 --- /dev/null +++ b/project/demos/face/Player.gd @@ -0,0 +1,32 @@ +extends KinematicBody2D + + +onready var collision_shape: = $CollisionShape2D +onready var agent: = GSTAgentLocation.new() + +export var speed: = 125.0 + +var _radius: = 0.0 + + +func _ready() -> void: + _radius = collision_shape.shape.radius + + +func _draw() -> void: + draw_circle(Vector2.ZERO, _radius, Color.red) + + +func _physics_process(delta: float) -> void: + var movement: = _get_movement() + move_and_slide(movement * speed) + _update_agent() + + +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")) + + +func _update_agent() -> void: + agent.position = Vector3(global_position.x, global_position.y, 0) diff --git a/project/demos/face/Turret.gd b/project/demos/face/Turret.gd new file mode 100644 index 0000000..9209af4 --- /dev/null +++ b/project/demos/face/Turret.gd @@ -0,0 +1,59 @@ +extends KinematicBody2D + + +onready var collision_shape: = $CollisionShape2D + +var _radius: = 0.0 +var _cannon: Rect2 + +var _agent: = GSTSteeringAgent.new() +var _accel: = GSTTargetAcceleration.new() + +var _angular_velocity: = 0.0 +var _angular_drag: = 1.0 +var _face: GSTFace + + +func _ready() -> void: + _radius = collision_shape.shape.radius + _cannon = Rect2(Vector2(-5, 0), Vector2(10, -_radius*2)) + + +func _draw() -> void: + draw_rect(_cannon, Color.blue) + draw_circle(Vector2.ZERO, _radius, Color.teal) + + +func _physics_process(delta: float) -> void: + if not _face: + return + + _accel = _face.calculate_steering(_accel) + _angular_velocity += _accel.angular + + if _angular_velocity < 0: + _angular_velocity += _angular_drag * delta + elif _angular_velocity > 0: + _angular_velocity -= _angular_drag * delta + + rotation += _angular_velocity * delta + + _update_agent() + + +func setup() -> void: + _face = GSTFace.new(_agent, owner.player.agent) + + _face.alignment_tolerance = 0.1 + _face.deceleration_radius = PI/2 + + _agent.max_angular_acceleration = 0.5 + _agent.max_angular_speed = 5 + _agent.position = Vector3(global_position.x, global_position.y, 0) + + _update_agent() + + +func _update_agent() -> void: + _agent.angular_velocity = _angular_velocity + _agent.orientation = rotation diff --git a/project/demos/follow_path/FollowPathDemo.tscn b/project/demos/follow_path/FollowPathDemo.tscn new file mode 100644 index 0000000..ee075a6 --- /dev/null +++ b/project/demos/follow_path/FollowPathDemo.tscn @@ -0,0 +1,3 @@ +[gd_scene format=2] + +[node name="FollowPathDemo" type="Node2D"] diff --git a/project/demos/group_behaviors/GroupBehaviorsDemo.tscn b/project/demos/group_behaviors/GroupBehaviorsDemo.tscn new file mode 100644 index 0000000..806d7ec --- /dev/null +++ b/project/demos/group_behaviors/GroupBehaviorsDemo.tscn @@ -0,0 +1,11 @@ +[gd_scene load_steps=3 format=2] + +[ext_resource path="res://demos/group_behaviors/Member.tscn" type="PackedScene" id=1] +[ext_resource path="res://demos/group_behaviors/Spawner.gd" type="Script" id=2] + +[node name="GroupBehaviorsDemo" type="Node2D"] + +[node name="Spawner" type="Node2D" parent="."] +position = Vector2( 397, 207 ) +script = ExtResource( 2 ) +member = ExtResource( 1 ) diff --git a/project/demos/group_behaviors/Member.gd b/project/demos/group_behaviors/Member.gd new file mode 100644 index 0000000..9e4b8b3 --- /dev/null +++ b/project/demos/group_behaviors/Member.gd @@ -0,0 +1,51 @@ +extends KinematicBody2D + + +var agent: = GSTSteeringAgent.new() +var separation: GSTSeparation +var cohesion: GSTCohesion +var proximity: GSTRadiusProximity +var blend: = GSTBlend.new(agent) +var acceleration: = GSTTargetAcceleration.new() + +var _radius: float +var _color: = Color.red +var _velocity: = Vector2() + +onready var shape: = $CollisionShape2D + + +func _ready() -> void: + _radius = shape.shape.radius + _color = Color(rand_range(0.5, 1), rand_range(0.25, 1), rand_range(0, 1)) + agent.max_linear_acceleration = 7 + agent.max_linear_speed = 70 + + proximity = GSTRadiusProximity.new(agent, [], 140) + separation = GSTSeparation.new(agent, proximity) + separation.decay_coefficient = 2000 + cohesion = GSTCohesion.new(agent, proximity) + blend.add(separation, 1.5) + blend.add(cohesion, 0.3) + + +func _draw() -> void: + draw_circle(Vector2.ZERO, _radius, _color) + + +func _process(delta: float) -> void: + update_agent() + if blend: + acceleration = blend.calculate_steering(acceleration) + _velocity = (_velocity + Vector2(acceleration.linear.x, acceleration.linear.y)).clamped(agent.max_linear_speed) + move_and_slide(_velocity) + + +func set_neighbors(neighbor: Array) -> void: + proximity.agents = neighbor + + +func update_agent() -> void: + var current_position: = global_position + agent.position.x = current_position.x + agent.position.y = current_position.y diff --git a/project/demos/group_behaviors/Member.tscn b/project/demos/group_behaviors/Member.tscn new file mode 100644 index 0000000..c2ae670 --- /dev/null +++ b/project/demos/group_behaviors/Member.tscn @@ -0,0 +1,12 @@ +[gd_scene load_steps=3 format=2] + +[ext_resource path="res://demos/group_behaviors/Member.gd" type="Script" id=1] + +[sub_resource type="CircleShape2D" id=1] +radius = 15.0 + +[node name="Member" type="KinematicBody2D"] +script = ExtResource( 1 ) + +[node name="CollisionShape2D" type="CollisionShape2D" parent="."] +shape = SubResource( 1 ) diff --git a/project/demos/group_behaviors/Spawner.gd b/project/demos/group_behaviors/Spawner.gd new file mode 100644 index 0000000..c1d8556 --- /dev/null +++ b/project/demos/group_behaviors/Spawner.gd @@ -0,0 +1,18 @@ +extends Node2D + + +export var member: PackedScene + + +func _ready() -> void: + var followers: = [] + for i in range(19): + var follower: = member.instance() + add_child(follower) + follower.position += Vector2(rand_range(-60, 60), rand_range(-60, 60)) + followers.append(follower) + var agents: = [] + for i in followers: + agents.append(i.agent) + for i in followers: + i.proximity.agents = agents diff --git a/project/demos/pursue_vs_seek/BoundaryManager.gd b/project/demos/pursue_vs_seek/BoundaryManager.gd new file mode 100644 index 0000000..a73b32f --- /dev/null +++ b/project/demos/pursue_vs_seek/BoundaryManager.gd @@ -0,0 +1,67 @@ +extends Node2D +# Wraps the ships' positions around the world border, and controls their rendering clones. + + +onready var ShipType: = preload("res://demos/pursue_vs_seek/Ship.gd") +onready var ships: = [$Player, $Pursuer, $Seeker] + +var _clones: = {} +var _world_bounds: Vector2 + + +func _ready() -> void: + _world_bounds = Vector2( + ProjectSettings["display/window/size/width"], + ProjectSettings["display/window/size/height"] + ) + + for i in range(ships.size()): + var ship: Node2D = ships[i] + var world_pos: = ship.position + + for i in range(3): + var ship_clone: = ShipType.new() + + ship_clone.position.x = world_pos.x if i == 1 else (world_pos.x - _world_bounds.x) + ship_clone.position.y = world_pos.y if i == 0 else (world_pos.y - _world_bounds.y) + ship_clone.rotation = ship.rotation + ship_clone.tag = i + ship_clone.name = ship.name + "Clone" + + add_child(ship_clone) + ship_clone.generate_sprite(ship.get_node("Sprite")) + _clones[ship_clone] = ship + + +func _physics_process(delta: float) -> void: + for clone in _clones.keys(): + var original: Node2D = _clones[clone] + var world_pos: Vector2 = original.position + + if world_pos.y < 0: + original.position.y = _world_bounds.y + world_pos.y + elif world_pos.y > _world_bounds.y: + original.position.y = (world_pos.y - _world_bounds.y) + + if world_pos.x < 0: + original.position.x = _world_bounds.x + world_pos.x + elif world_pos.x > _world_bounds.x: + original.position.x = (world_pos.x - _world_bounds.x) + + var tag: int = clone.tag + if tag != 2: + if world_pos.x < _world_bounds.x/2: + clone.position.x = world_pos.x + _world_bounds.x + else: + clone.position.x = world_pos.x - _world_bounds.x + else: + clone.position.x = world_pos.x + + if tag != 0: + if world_pos.y < _world_bounds.y/2: + clone.position.y = world_pos.y + _world_bounds.y + else: + clone.position.y = world_pos.y - _world_bounds.y + else: + clone.position.y = world_pos.y + clone.rotation = original.rotation diff --git a/project/demos/pursue_vs_seek/GUI.gd b/project/demos/pursue_vs_seek/GUI.gd new file mode 100644 index 0000000..467c9e2 --- /dev/null +++ b/project/demos/pursue_vs_seek/GUI.gd @@ -0,0 +1,38 @@ +extends PanelContainer + + +signal linear_speed_changed(value) +signal linear_accel_changed(value) +signal angular_speed_changed(value) +signal angular_accel_changed(value) +signal decel_radius_changed(value) +signal predict_time_changed(value) + + +onready var linear_speed: = $GUI/Controls/LinSpeedBox/MaxLinSpeed +onready var lin_speed_label: = $GUI/Controls/LinSpeedBox/Label +onready var linear_accel: = $GUI/Controls/LinAccelBox/MaxLinAccel +onready var lin_accel_label: = $GUI/Controls/LinAccelBox/Label +onready var predict_time: = $GUI/Controls/PredictTime/PredictTime +onready var predict_time_label: = $GUI/Controls/PredictTime/Label + + +func _ready() -> void: + linear_speed.connect("value_changed", self, "_on_Slider_linear_speed_changed") + linear_accel.connect("value_changed", self, "_on_Slider_linear_accel_changed") + predict_time.connect("value_changed", self, "_on_Slider_predict_time_changed") + + +func _on_Slider_linear_speed_changed(value: float) -> void: + lin_speed_label.text = "Max linear speed (" + str(value) + ")" + emit_signal("linear_speed_changed", value) + + +func _on_Slider_linear_accel_changed(value: float) -> void: + lin_accel_label.text = "Max linear accel (" + str(value) + ")" + emit_signal("linear_accel_changed", value) + + +func _on_Slider_predict_time_changed(value: float) -> void: + predict_time_label.text = "Predict time (" + str(value) + " sec)" + emit_signal("predict_time_changed", value) diff --git a/project/demos/pursue_vs_seek/GUI.tscn b/project/demos/pursue_vs_seek/GUI.tscn new file mode 100644 index 0000000..4eda7ac --- /dev/null +++ b/project/demos/pursue_vs_seek/GUI.tscn @@ -0,0 +1,139 @@ +[gd_scene load_steps=3 format=2] + +[ext_resource path="res://demos/pursue_vs_seek/GUI.gd" type="Script" id=1] +[ext_resource path="res://assets/theme/gdquest.theme" type="Theme" id=2] + +[node name="PanelContainer" type="PanelContainer"] +anchor_bottom = 1.0 +margin_right = 364.0 +theme = ExtResource( 2 ) +script = ExtResource( 1 ) +__meta__ = { +"_edit_use_anchors_": false +} + +[node name="GUI" type="MarginContainer" parent="."] +margin_right = 364.0 +margin_bottom = 600.0 +custom_constants/margin_right = 20 +custom_constants/margin_top = 20 +custom_constants/margin_left = 20 +custom_constants/margin_bottom = 20 +__meta__ = { +"_edit_use_anchors_": false +} + +[node name="Controls" type="VBoxContainer" parent="GUI"] +margin_left = 20.0 +margin_top = 20.0 +margin_right = 344.0 +margin_bottom = 580.0 + +[node name="LinSpeedBox" type="VBoxContainer" parent="GUI/Controls"] +margin_right = 324.0 +margin_bottom = 50.0 + +[node name="Label" type="Label" parent="GUI/Controls/LinSpeedBox"] +margin_right = 324.0 +margin_bottom = 26.0 +text = "Max linear speed (2000)" + +[node name="MaxLinSpeed" type="HSlider" parent="GUI/Controls/LinSpeedBox"] +margin_top = 34.0 +margin_right = 324.0 +margin_bottom = 50.0 +max_value = 500.0 + +[node name="LinAccelBox" type="VBoxContainer" parent="GUI/Controls"] +margin_top = 58.0 +margin_right = 324.0 +margin_bottom = 108.0 + +[node name="Label" type="Label" parent="GUI/Controls/LinAccelBox"] +margin_right = 324.0 +margin_bottom = 26.0 +text = "Max linear accel (2000)" + +[node name="MaxLinAccel" type="HSlider" parent="GUI/Controls/LinAccelBox"] +margin_top = 34.0 +margin_right = 324.0 +margin_bottom = 50.0 +step = 0.5 + +[node name="PredictTime" type="VBoxContainer" parent="GUI/Controls"] +margin_top = 116.0 +margin_right = 324.0 +margin_bottom = 166.0 + +[node name="Label" type="Label" parent="GUI/Controls/PredictTime"] +margin_right = 324.0 +margin_bottom = 26.0 +text = "Predict time (2000)" + +[node name="PredictTime" type="HSlider" parent="GUI/Controls/PredictTime"] +margin_top = 34.0 +margin_right = 324.0 +margin_bottom = 50.0 +max_value = 5.0 +step = 0.1 + +[node name="Help" type="VBoxContainer" parent="GUI/Controls"] +margin_top = 174.0 +margin_right = 324.0 +margin_bottom = 264.0 + +[node name="Controls" type="Label" parent="GUI/Controls/Help"] +margin_right = 324.0 +margin_bottom = 26.0 +text = "Controls" + +[node name="GridContainer" type="GridContainer" parent="GUI/Controls/Help"] +margin_top = 34.0 +margin_right = 324.0 +margin_bottom = 90.0 +columns = 3 + +[node name="Sep" type="Control" parent="GUI/Controls/Help/GridContainer"] +margin_right = 15.0 +margin_bottom = 26.0 +rect_min_size = Vector2( 15, 15 ) + +[node name="W" type="Label" parent="GUI/Controls/Help/GridContainer"] +margin_left = 19.0 +margin_right = 42.0 +margin_bottom = 26.0 +rect_min_size = Vector2( 15, 15 ) +text = "W" +align = 1 + +[node name="Sep2" type="Control" parent="GUI/Controls/Help/GridContainer"] +margin_left = 46.0 +margin_right = 63.0 +margin_bottom = 26.0 +rect_min_size = Vector2( 15, 15 ) + +[node name="A" type="Label" parent="GUI/Controls/Help/GridContainer"] +margin_top = 30.0 +margin_right = 15.0 +margin_bottom = 56.0 +rect_min_size = Vector2( 15, 15 ) +text = "A" +align = 1 + +[node name="S" type="Label" parent="GUI/Controls/Help/GridContainer"] +margin_left = 19.0 +margin_top = 30.0 +margin_right = 42.0 +margin_bottom = 56.0 +rect_min_size = Vector2( 15, 15 ) +text = "S" +align = 1 + +[node name="D" type="Label" parent="GUI/Controls/Help/GridContainer"] +margin_left = 46.0 +margin_top = 30.0 +margin_right = 63.0 +margin_bottom = 56.0 +rect_min_size = Vector2( 15, 15 ) +text = "D" +align = 1 diff --git a/project/demos/pursue_vs_seek/Player.gd b/project/demos/pursue_vs_seek/Player.gd new file mode 100644 index 0000000..29df3ed --- /dev/null +++ b/project/demos/pursue_vs_seek/Player.gd @@ -0,0 +1,101 @@ +extends "res://demos/pursue_vs_seek/Ship.gd" +# Controls the player ship's movements based on player input. + + +onready var agent: = GSTSteeringAgent.new() + +export var thruster_strength: = 150.0 +export var side_thruster_strength: = 10.0 +export var max_velocity: = 150.0 +export var max_angular_velocity: = 2.0 +export var angular_drag: = 5.0 +export var linear_drag: = 100.0 + +var _linear_velocity: = Vector2() +var _angular_velocity: = 0.0 + + +func _physics_process(delta: float) -> void: + var movement: = _get_movement() + _angular_velocity = _calculate_angular_velocity( + movement.x, + _angular_velocity, + side_thruster_strength, + max_angular_velocity, + angular_drag, + delta + ) + rotation += (_angular_velocity * delta) + + _linear_velocity = _calculate_linear_velocity( + movement.y, + _linear_velocity, + Vector2.UP.rotated(rotation), + linear_drag, + thruster_strength, + max_velocity, + delta + ) + + _linear_velocity = move_and_slide(_linear_velocity) + + _update_agent(_linear_velocity, rotation) + + +func _calculate_angular_velocity( + horizontal_movement: float, + current_velocity: float, + thruster_strength: float, + max_velocity: float, + ship_drag: float, + delta: float) -> float: + + var velocity: = clamp( + current_velocity + thruster_strength * horizontal_movement * delta, + -max_velocity, + max_velocity + ) + + if velocity > 0: + velocity -= ship_drag * delta + elif velocity < 0: + velocity += ship_drag * delta + if abs(velocity) < 0.01: + velocity = 0 + + return velocity + + +func _calculate_linear_velocity( + vertical_movement: float, + current_velocity: Vector2, + facing_direction: Vector2, + ship_drag: float, + strength: float, + max_speed: 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 -= current_velocity.normalized() * (ship_drag * delta) + + return velocity.clamped(max_speed) + + +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(velocity: Vector2, orientation: float) -> void: + agent.position.x = global_position.x + agent.position.y = global_position.y + agent.linear_velocity.x = velocity.x + agent.linear_velocity.y = velocity.y + agent.angular_velocity = _angular_velocity + agent.orientation = orientation diff --git a/project/demos/pursue_vs_seek/PursueVSSeekDemo.gd b/project/demos/pursue_vs_seek/PursueVSSeekDemo.gd new file mode 100644 index 0000000..e65d583 --- /dev/null +++ b/project/demos/pursue_vs_seek/PursueVSSeekDemo.gd @@ -0,0 +1,34 @@ +extends Node2D + + +onready var gui: = $GUI +onready var pursuer: = $BoundaryManager/Pursuer +onready var seeker: = $BoundaryManager/Seeker + +export var start_linear_speed: = 200.0 +export var start_linear_accel: = 25.0 +export var start_predict_time: = 0.3 + + +func _ready() -> void: + gui.connect("linear_accel_changed", self, "_on_GUI_linear_accel_changed") + gui.connect("linear_speed_changed", self, "_on_GUI_linear_speed_changed") + gui.connect("predict_time_changed", self, "_on_GUI_predict_time_changed") + yield(get_tree(), "idle_frame") + gui.linear_speed.value = start_linear_speed + gui.linear_accel.value = start_linear_accel + gui.predict_time.value = start_predict_time + + +func _on_GUI_linear_accel_changed(value: int) -> void: + pursuer.agent.max_linear_acceleration = float(value) + seeker.agent.max_linear_acceleration = float(value) + + +func _on_GUI_linear_speed_changed(value: int) -> void: + pursuer.agent.max_linear_speed = float(value) + seeker.agent.max_linear_speed = float(value) + + +func _on_GUI_predict_time_changed(value: int) -> void: + pursuer._behavior.max_predict_time = float(value) diff --git a/project/demos/pursue_vs_seek/PursueVSSeekDemo.tscn b/project/demos/pursue_vs_seek/PursueVSSeekDemo.tscn new file mode 100644 index 0000000..d9d2437 --- /dev/null +++ b/project/demos/pursue_vs_seek/PursueVSSeekDemo.tscn @@ -0,0 +1,61 @@ +[gd_scene load_steps=7 format=2] + +[ext_resource path="res://demos/pursue_vs_seek/Pursuer.gd" type="Script" id=1] +[ext_resource path="res://demos/pursue_vs_seek/Player.gd" type="Script" id=2] +[ext_resource path="res://demos/pursue_vs_seek/BoundaryManager.gd" type="Script" id=3] +[ext_resource path="res://demos/pursue_vs_seek/PursueVSSeekDemo.gd" type="Script" id=4] +[ext_resource path="res://demos/pursue_vs_seek/GUI.tscn" type="PackedScene" id=5] +[ext_resource path="res://assets/sprites/triangle.png" type="Texture" id=6] + +[node name="PursueVSSeekDemo" type="Node2D"] +script = ExtResource( 4 ) +__meta__ = { +"_editor_description_": "Toy demo to demonstrate the use of the Pursue contrasted to the more naive Seek steering behavior." +} +start_linear_speed = 150.0 +start_linear_accel = 75.0 +start_predict_time = 2.0 + +[node name="BoundaryManager" type="Node2D" parent="."] +script = ExtResource( 3 ) + +[node name="Player" type="KinematicBody2D" parent="BoundaryManager"] +position = Vector2( 49.2031, 556.936 ) +rotation = 1.5708 +collision_mask = 2 +script = ExtResource( 2 ) + +[node name="CollisionPolygon2D" type="CollisionPolygon2D" parent="BoundaryManager/Player"] +polygon = PoolVector2Array( 0, -32, -24, 32, 24, 32 ) + +[node name="Sprite" type="Sprite" parent="BoundaryManager/Player"] +modulate = Color( 0.968627, 0.188235, 0.0352941, 1 ) +texture = ExtResource( 6 ) + +[node name="Pursuer" type="KinematicBody2D" parent="BoundaryManager"] +position = Vector2( 868.495, 87.9043 ) +collision_layer = 2 +script = ExtResource( 1 ) + +[node name="CollisionPolygon2D" type="CollisionPolygon2D" parent="BoundaryManager/Pursuer"] +polygon = PoolVector2Array( 0, -32, -24, 32, 24, 32 ) + +[node name="Sprite" type="Sprite" parent="BoundaryManager/Pursuer"] +modulate = Color( 0.756863, 0.952941, 0.054902, 1 ) +texture = ExtResource( 6 ) + +[node name="Seeker" type="KinematicBody2D" parent="BoundaryManager"] +position = Vector2( 821.24, 87.9043 ) +collision_layer = 2 +script = ExtResource( 1 ) +use_seek = true + +[node name="CollisionPolygon2D" type="CollisionPolygon2D" parent="BoundaryManager/Seeker"] +polygon = PoolVector2Array( 0, -32, -24, 32, 24, 32 ) + +[node name="Sprite" type="Sprite" parent="BoundaryManager/Seeker"] +modulate = Color( 0.278431, 0.815686, 0.14902, 1 ) +texture = ExtResource( 6 ) + +[node name="GUI" parent="." instance=ExtResource( 5 )] +margin_right = 309.0 diff --git a/project/demos/pursue_vs_seek/Pursuer.gd b/project/demos/pursue_vs_seek/Pursuer.gd new file mode 100644 index 0000000..12ea4a6 --- /dev/null +++ b/project/demos/pursue_vs_seek/Pursuer.gd @@ -0,0 +1,66 @@ +extends "res://demos/pursue_vs_seek/Ship.gd" +# Represents a ship that chases after the player. + + +onready var agent: = GSTSteeringAgent.new() +onready var accel: = GSTTargetAcceleration.new() +onready var player_agent: GSTSteeringAgent = owner.find_node("Player", true, false).agent + +export var use_seek: bool = false + +var _orient_behavior: GSTSteeringBehavior +var _behavior: GSTSteeringBehavior + +var _linear_velocity: = Vector2() +var _angular_velocity: = 0.0 +var _angular_drag: = 1.0 + + +func _ready() -> void: + _setup() + + +func _setup() -> void: + if use_seek: + _behavior = GSTSeek.new(agent, player_agent) + else: + _behavior = GSTPursue.new(agent, player_agent, 2) + + _orient_behavior = GSTLookWhereYouGo.new(agent) + _orient_behavior.alignment_tolerance = 0.001 + _orient_behavior.deceleration_radius = PI/2 + + agent.max_angular_acceleration = 2 + agent.max_angular_speed = 5 + agent.max_linear_acceleration = 75 + agent.max_linear_speed = 125 + _update_agent() + + +func _physics_process(delta: float) -> void: + accel = _orient_behavior.calculate_steering(accel) + _angular_velocity += accel.angular + + if _angular_velocity < 0: + _angular_velocity += _angular_drag * delta + elif _angular_velocity > 0: + _angular_velocity -= _angular_drag * delta + + rotation = rotation + _angular_velocity * delta + + accel = _behavior.calculate_steering(accel) + _linear_velocity += Vector2(accel.linear.x, accel.linear.y) * delta + _linear_velocity -= _linear_velocity.normalized() * 10 * delta + _linear_velocity = _linear_velocity.clamped(agent.max_linear_speed) + _linear_velocity = move_and_slide(_linear_velocity) + + _update_agent() + + +func _update_agent() -> void: + agent.position.x = global_position.x + agent.position.y = global_position.y + agent.orientation = rotation + agent.linear_velocity.x = _linear_velocity.x + agent.linear_velocity.y = _linear_velocity.y + agent.angular_velocity = _angular_velocity diff --git a/project/demos/pursue_vs_seek/Ship.gd b/project/demos/pursue_vs_seek/Ship.gd new file mode 100644 index 0000000..194c329 --- /dev/null +++ b/project/demos/pursue_vs_seek/Ship.gd @@ -0,0 +1,13 @@ +extends KinematicBody2D +# Represents a basic ship + + +var tag: int = 0 + + +func generate_sprite(sprite: Sprite) -> void: + var new_sprite = Sprite.new() + new_sprite.texture = sprite.texture + new_sprite.modulate = sprite.modulate + new_sprite.name = "Sprite" + add_child(new_sprite) diff --git a/project/demos/seek_and_flee/Boundary.gd b/project/demos/seek_and_flee/Boundary.gd new file mode 100644 index 0000000..b5a9c03 --- /dev/null +++ b/project/demos/seek_and_flee/Boundary.gd @@ -0,0 +1,14 @@ +extends StaticBody2D +# Draws the bounding box of the static body wall. + + +var rect: Rect2 + + +func _ready() -> void: + var extents: = ($CollisionShape2D.shape as RectangleShape2D).extents + rect = Rect2(-extents, extents*2) + + +func _draw() -> void: + draw_rect(rect, Color.yellowgreen) diff --git a/project/demos/seek_and_flee/GUI.gd b/project/demos/seek_and_flee/GUI.gd new file mode 100644 index 0000000..c3750c8 --- /dev/null +++ b/project/demos/seek_and_flee/GUI.gd @@ -0,0 +1,48 @@ +extends PanelContainer + + +enum BehaviorMode { SEEK, FLEE } + +signal mode_changed(behavior_mode) +signal accel_changed(value) +signal speed_changed(value) + +onready var seek: = $MarginContainer/BehaviorControls/Seek +onready var flee: = $MarginContainer/BehaviorControls/Flee +onready var max_accel: = $MarginContainer/BehaviorControls/MaxAccelValue +onready var max_speed: = $MarginContainer/BehaviorControls/MaxSpeedValue +onready var max_accel_label: = $MarginContainer/BehaviorControls/MaxAccel +onready var max_speed_label: = $MarginContainer/BehaviorControls/MaxSpeed + + +func _ready() -> void: + seek.connect("pressed", self, "_on_Seek_pressed") + flee.connect("pressed", self, "_on_Flee_pressed") + max_accel.connect("value_changed", self, "_on_Accel_changed") + max_speed.connect("value_changed", self, "_on_Speed_changed") + max_accel_label.text = "Max accel (" + str(max_accel.value) + ")" + max_speed_label.text = "Max speed (" + str(max_speed.value) + ")" + + +func _on_Seek_pressed() -> void: + flee.pressed = false + flee.button_mask = BUTTON_MASK_LEFT + seek.button_mask = 0 + emit_signal("mode_changed", BehaviorMode.SEEK) + + +func _on_Flee_pressed() -> void: + seek.pressed = false + seek.button_mask = BUTTON_MASK_LEFT + flee.button_mask = 0 + emit_signal("mode_changed", BehaviorMode.FLEE) + + +func _on_Accel_changed(value: float) -> void: + max_accel_label.text = "Max accel (" + str(value) + ")" + emit_signal("accel_changed", value) + + +func _on_Speed_changed(value: float) -> void: + max_speed_label.text = "Max speed (" + str(value) + ")" + emit_signal("speed_changed", value) diff --git a/project/demos/seek_and_flee/GUI.tscn b/project/demos/seek_and_flee/GUI.tscn new file mode 100644 index 0000000..a63d376 --- /dev/null +++ b/project/demos/seek_and_flee/GUI.tscn @@ -0,0 +1,142 @@ +[gd_scene load_steps=3 format=2] + +[ext_resource path="res://assets/theme/gdquest.theme" type="Theme" id=1] +[ext_resource path="res://demos/seek_and_flee/GUI.gd" type="Script" id=3] + +[node name="GUI" type="PanelContainer"] +anchor_bottom = 1.0 +margin_right = 118.0 +theme = ExtResource( 1 ) +script = ExtResource( 3 ) +__meta__ = { +"_edit_use_anchors_": false +} + +[node name="MarginContainer" type="MarginContainer" parent="."] +margin_right = 218.0 +margin_bottom = 600.0 +custom_constants/margin_right = 20 +custom_constants/margin_top = 20 +custom_constants/margin_left = 20 +custom_constants/margin_bottom = 20 +__meta__ = { +"_edit_use_anchors_": false +} + +[node name="BehaviorControls" type="VBoxContainer" parent="MarginContainer"] +margin_left = 20.0 +margin_top = 20.0 +margin_right = 198.0 +margin_bottom = 580.0 + +[node name="Seek" type="CheckBox" parent="MarginContainer/BehaviorControls"] +margin_right = 178.0 +margin_bottom = 26.0 +focus_mode = 0 +pressed = true +enabled_focus_mode = 0 +text = "Seek" + +[node name="Flee" type="CheckBox" parent="MarginContainer/BehaviorControls"] +margin_top = 34.0 +margin_right = 178.0 +margin_bottom = 60.0 +focus_mode = 0 +enabled_focus_mode = 0 +text = "Flee" + +[node name="MaxAccel" type="Label" parent="MarginContainer/BehaviorControls"] +margin_top = 68.0 +margin_right = 178.0 +margin_bottom = 94.0 +text = "Max accel (2000)" +__meta__ = { +"_edit_use_anchors_": false +} + +[node name="MaxAccelValue" type="HSlider" parent="MarginContainer/BehaviorControls"] +margin_top = 102.0 +margin_right = 178.0 +margin_bottom = 118.0 +max_value = 250.0 +step = 0.5 +value = 20.0 + +[node name="MaxSpeed" type="Label" parent="MarginContainer/BehaviorControls"] +margin_top = 126.0 +margin_right = 178.0 +margin_bottom = 152.0 +text = "Max speed (2000)" +__meta__ = { +"_edit_use_anchors_": false +} + +[node name="MaxSpeedValue" type="HSlider" parent="MarginContainer/BehaviorControls"] +margin_top = 160.0 +margin_right = 178.0 +margin_bottom = 176.0 +max_value = 450.0 +value = 100.0 + +[node name="Help" type="VBoxContainer" parent="MarginContainer/BehaviorControls"] +margin_top = 184.0 +margin_right = 178.0 +margin_bottom = 274.0 + +[node name="Controls" type="Label" parent="MarginContainer/BehaviorControls/Help"] +margin_right = 178.0 +margin_bottom = 26.0 +text = "Controls" + +[node name="GridContainer" type="GridContainer" parent="MarginContainer/BehaviorControls/Help"] +margin_left = 57.0 +margin_top = 34.0 +margin_right = 120.0 +margin_bottom = 90.0 +size_flags_horizontal = 4 +columns = 3 + +[node name="Sep" type="Control" parent="MarginContainer/BehaviorControls/Help/GridContainer"] +margin_right = 15.0 +margin_bottom = 26.0 +rect_min_size = Vector2( 15, 15 ) + +[node name="W" type="Label" parent="MarginContainer/BehaviorControls/Help/GridContainer"] +margin_left = 19.0 +margin_right = 42.0 +margin_bottom = 26.0 +rect_min_size = Vector2( 15, 15 ) +text = "W" +align = 1 + +[node name="Sep2" type="Control" parent="MarginContainer/BehaviorControls/Help/GridContainer"] +margin_left = 46.0 +margin_right = 63.0 +margin_bottom = 26.0 +rect_min_size = Vector2( 15, 15 ) + +[node name="A" type="Label" parent="MarginContainer/BehaviorControls/Help/GridContainer"] +margin_top = 30.0 +margin_right = 15.0 +margin_bottom = 56.0 +rect_min_size = Vector2( 15, 15 ) +text = "A" +align = 1 + +[node name="S" type="Label" parent="MarginContainer/BehaviorControls/Help/GridContainer"] +margin_left = 19.0 +margin_top = 30.0 +margin_right = 42.0 +margin_bottom = 56.0 +rect_min_size = Vector2( 15, 15 ) +text = "S" +align = 1 + +[node name="D" type="Label" parent="MarginContainer/BehaviorControls/Help/GridContainer"] +margin_left = 46.0 +margin_top = 30.0 +margin_right = 63.0 +margin_bottom = 56.0 +rect_min_size = Vector2( 15, 15 ) +text = "D" +align = 1 diff --git a/project/demos/seek_and_flee/Player.gd b/project/demos/seek_and_flee/Player.gd new file mode 100644 index 0000000..485b88e --- /dev/null +++ b/project/demos/seek_and_flee/Player.gd @@ -0,0 +1,21 @@ +extends KinematicBody2D +# Class to control the player in basic left/right up/down movement. + + +onready var agent: = GSTAgentLocation.new() + +export var speed: = 150.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")) + + +func _physics_process(delta: float) -> void: + var movement: = _get_movement() + if movement.length_squared() < 0.01: + return + + move_and_slide(movement * speed) + agent.position = Vector3(global_position.x, global_position.y, 0) diff --git a/project/demos/seek_and_flee/SeekFleeDemo.gd b/project/demos/seek_and_flee/SeekFleeDemo.gd new file mode 100644 index 0000000..9fcd168 --- /dev/null +++ b/project/demos/seek_and_flee/SeekFleeDemo.gd @@ -0,0 +1,42 @@ +extends Node2D +# Access helper class for children to access window boundaries. + + +onready var player: KinematicBody2D = $Player +onready var spawner: Node2D = $Spawner +onready var gui: = $GUI + +var camera_boundaries: Rect2 + + +func _init() -> void: + camera_boundaries = Rect2( + Vector2.ZERO, + Vector2( + ProjectSettings["display/window/size/width"], + ProjectSettings["display/window/size/height"] + ) + ) + + +func _ready() -> void: + var rng: = RandomNumberGenerator.new() + rng.randomize() + + gui.max_accel.value = spawner.max_accel + gui.max_speed.value = spawner.max_speed + + for i in range(spawner.entity_count): + var new_pos: = Vector2( + rng.randf_range(-camera_boundaries.size.x/2, camera_boundaries.size.x/2), + rng.randf_range(-camera_boundaries.size.y/2, camera_boundaries.size.y/2) + ) + var entity: KinematicBody2D = spawner.Entity.instance() + entity.global_position = new_pos + entity.player_agent = player.agent + entity.start_speed = spawner.max_speed + entity.start_accel = spawner.max_accel + gui.connect("mode_changed", entity, "_on_GUI_mode_changed") + gui.connect("accel_changed", entity, "_on_GUI_accel_changed") + gui.connect("speed_changed", entity, "_on_GUI_speed_changed") + spawner.add_child(entity) diff --git a/project/demos/seek_and_flee/SeekFleeDemo.tscn b/project/demos/seek_and_flee/SeekFleeDemo.tscn new file mode 100644 index 0000000..02a7a48 --- /dev/null +++ b/project/demos/seek_and_flee/SeekFleeDemo.tscn @@ -0,0 +1,86 @@ +[gd_scene load_steps=11 format=2] + +[ext_resource path="res://demos/seek_and_flee/Boundary.gd" type="Script" id=1] +[ext_resource path="res://demos/seek_and_flee/Player.gd" type="Script" id=2] +[ext_resource path="res://demos/seek_and_flee/SeekFleeDemo.gd" type="Script" id=3] +[ext_resource path="res://demos/seek_and_flee/Spawner.gd" type="Script" id=4] +[ext_resource path="res://demos/seek_and_flee/GUI.tscn" type="PackedScene" id=5] +[ext_resource path="res://demos/seek_and_flee/Seeker.tscn" type="PackedScene" id=6] +[ext_resource path="res://assets/sprites/large_circle.png" type="Texture" id=7] + +[sub_resource type="CircleShape2D" id=1] +radius = 32.0 + +[sub_resource type="RectangleShape2D" id=2] +extents = Vector2( 10, 300 ) + +[sub_resource type="RectangleShape2D" id=3] +extents = Vector2( 512, 10 ) + +[node name="SeekFleeDemo" type="Node2D"] +script = ExtResource( 3 ) +__meta__ = { +"_editor_description_": "A toy demo to demonstrate the usage for the Seek and Flee steering behaviors." +} + +[node name="Camera2D" type="Camera2D" parent="."] +current = true + +[node name="Player" type="KinematicBody2D" parent="."] +collision_mask = 2 +script = ExtResource( 2 ) + +[node name="CollisionShape2D" type="CollisionShape2D" parent="Player"] +shape = SubResource( 1 ) + +[node name="Sprite" type="Sprite" parent="Player"] +modulate = Color( 0.952941, 0.290196, 0.0588235, 1 ) +texture = ExtResource( 7 ) + +[node name="LeftBoundary" type="StaticBody2D" parent="."] +position = Vector2( -512, 0 ) +collision_layer = 2 +collision_mask = 5 +script = ExtResource( 1 ) + +[node name="CollisionShape2D" type="CollisionShape2D" parent="LeftBoundary"] +shape = SubResource( 2 ) + +[node name="RightBoundary" type="StaticBody2D" parent="."] +position = Vector2( 512, 0 ) +collision_layer = 2 +collision_mask = 5 +script = ExtResource( 1 ) + +[node name="CollisionShape2D" type="CollisionShape2D" parent="RightBoundary"] +shape = SubResource( 2 ) + +[node name="TopBoundary" type="StaticBody2D" parent="."] +position = Vector2( 0, -300 ) +collision_layer = 2 +collision_mask = 5 +script = ExtResource( 1 ) + +[node name="CollisionShape2D" type="CollisionShape2D" parent="TopBoundary"] +shape = SubResource( 3 ) + +[node name="BottomBoundary" type="StaticBody2D" parent="."] +position = Vector2( 0, 300 ) +collision_layer = 2 +collision_mask = 5 +script = ExtResource( 1 ) + +[node name="CollisionShape2D" type="CollisionShape2D" parent="BottomBoundary"] +shape = SubResource( 3 ) + +[node name="Spawner" type="Node2D" parent="."] +script = ExtResource( 4 ) +Entity = ExtResource( 6 ) +max_speed = 150.0 + +[node name="GUI" parent="." instance=ExtResource( 5 )] +anchor_bottom = 0.0 +margin_left = -512.0 +margin_top = -300.0 +margin_right = -294.0 +margin_bottom = 14.0 diff --git a/project/demos/seek_and_flee/Seeker.gd b/project/demos/seek_and_flee/Seeker.gd new file mode 100644 index 0000000..9374926 --- /dev/null +++ b/project/demos/seek_and_flee/Seeker.gd @@ -0,0 +1,52 @@ +extends KinematicBody2D +# AI agent that uses the Seek behavior to hone in on the player's location as directly as possible. + + +onready var agent: = GSTSteeringAgent.new() +onready var accel: = GSTTargetAcceleration.new() +onready var seek: = GSTSeek.new(agent, player_agent) +onready var flee: = GSTFlee.new(agent, player_agent) +onready var _active_behavior: = seek + +var player_agent: GSTAgentLocation +var velocity: = Vector2.ZERO +var start_speed: float +var start_accel: float + + +func _ready() -> void: + agent.max_linear_acceleration = start_accel + agent.max_linear_speed = start_speed + + +func _physics_process(delta: float) -> void: + if not player_agent: + return + + _update_agent() + accel = _active_behavior.calculate_steering(accel) + + velocity = (velocity + Vector2(accel.linear.x, accel.linear.y)).clamped(agent.max_linear_speed) + velocity = move_and_slide(velocity) + + +func _update_agent() -> void: + agent.position.x = global_position.x + agent.position.y = global_position.y + agent.linear_velocity.x = velocity.x + agent.linear_velocity.y = velocity.y + + +func _on_GUI_mode_changed(mode: int) -> void: + if mode == 0: + _active_behavior = seek + else: + _active_behavior = flee + + +func _on_GUI_accel_changed(value: float) -> void: + agent.max_linear_acceleration = value + + +func _on_GUI_speed_changed(value: float) -> void: + agent.max_linear_speed = value diff --git a/project/demos/seek_and_flee/Seeker.tscn b/project/demos/seek_and_flee/Seeker.tscn new file mode 100644 index 0000000..6e5d647 --- /dev/null +++ b/project/demos/seek_and_flee/Seeker.tscn @@ -0,0 +1,19 @@ +[gd_scene load_steps=4 format=2] + +[ext_resource path="res://demos/seek_and_flee/Seeker.gd" type="Script" id=1] +[ext_resource path="res://assets/sprites/small_circle.png" type="Texture" 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 ) + +[node name="Sprite" type="Sprite" parent="."] +modulate = Color( 0.113725, 0.635294, 0.94902, 1 ) +texture = ExtResource( 2 ) diff --git a/project/demos/seek_and_flee/Spawner.gd b/project/demos/seek_and_flee/Spawner.gd new file mode 100644 index 0000000..87555d7 --- /dev/null +++ b/project/demos/seek_and_flee/Spawner.gd @@ -0,0 +1,9 @@ +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 +export var max_speed: = 100.0 +export var max_accel: = 20.0 diff --git a/project/icon.png b/project/icon.png new file mode 100644 index 0000000..2b65815 Binary files /dev/null and b/project/icon.png differ diff --git a/project/icon.png.import b/project/icon.png.import new file mode 100644 index 0000000..96cbf46 --- /dev/null +++ b/project/icon.png.import @@ -0,0 +1,34 @@ +[remap] + +importer="texture" +type="StreamTexture" +path="res://.import/icon.png-487276ed1e3a0c39cad0279d744ee560.stex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://icon.png" +dest_files=[ "res://.import/icon.png-487276ed1e3a0c39cad0279d744ee560.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 +stream=false +size_limit=0 +detect_3d=true +svg/scale=1.0 diff --git a/project/project.godot b/project/project.godot new file mode 100644 index 0000000..fc258be --- /dev/null +++ b/project/project.godot @@ -0,0 +1,193 @@ +; 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": "Reference", +"class": "GSTAgentLocation", +"language": "GDScript", +"path": "res://src/GSTAgentLocation.gd" +}, { +"base": "GSTSteeringBehavior", +"class": "GSTArrive", +"language": "GDScript", +"path": "res://src/behaviors/GSTArrive.gd" +}, { +"base": "GSTGroupBehavior", +"class": "GSTAvoidCollisions", +"language": "GDScript", +"path": "res://src/behaviors/GSTAvoidCollisions.gd" +}, { +"base": "GSTSteeringBehavior", +"class": "GSTBlend", +"language": "GDScript", +"path": "res://src/behaviors/GSTBlend.gd" +}, { +"base": "GSTGroupBehavior", +"class": "GSTCohesion", +"language": "GDScript", +"path": "res://src/behaviors/GSTCohesion.gd" +}, { +"base": "GSTPursue", +"class": "GSTEvade", +"language": "GDScript", +"path": "res://src/behaviors/GSTEvade.gd" +}, { +"base": "GSTMatchOrientation", +"class": "GSTFace", +"language": "GDScript", +"path": "res://src/behaviors/GSTFace.gd" +}, { +"base": "GSTSeek", +"class": "GSTFlee", +"language": "GDScript", +"path": "res://src/behaviors/GSTFlee.gd" +}, { +"base": "GSTArrive", +"class": "GSTFollowPath", +"language": "GDScript", +"path": "res://src/behaviors/GSTFollowPath.gd" +}, { +"base": "GSTSteeringBehavior", +"class": "GSTGroupBehavior", +"language": "GDScript", +"path": "res://src/GSTGroupBehavior.gd" +}, { +"base": "GSTProximity", +"class": "GSTInfiniteProximity", +"language": "GDScript", +"path": "res://src/proximities/GSTInfiniteProximity.gd" +}, { +"base": "GSTMatchOrientation", +"class": "GSTLookWhereYouGo", +"language": "GDScript", +"path": "res://src/behaviors/GSTLookWhereYouGo.gd" +}, { +"base": "GSTSteeringBehavior", +"class": "GSTMatchOrientation", +"language": "GDScript", +"path": "res://src/behaviors/GSTMatchOrientation.gd" +}, { +"base": "Reference", +"class": "GSTPath", +"language": "GDScript", +"path": "res://src/GSTPath.gd" +}, { +"base": "GSTSteeringBehavior", +"class": "GSTPriority", +"language": "GDScript", +"path": "res://src/behaviors/GSTPriority.gd" +}, { +"base": "Reference", +"class": "GSTProximity", +"language": "GDScript", +"path": "res://src/proximities/GSTProximity.gd" +}, { +"base": "GSTSteeringBehavior", +"class": "GSTPursue", +"language": "GDScript", +"path": "res://src/behaviors/GSTPursue.gd" +}, { +"base": "GSTProximity", +"class": "GSTRadiusProximity", +"language": "GDScript", +"path": "res://src/proximities/GSTRadiusProximity.gd" +}, { +"base": "GSTSteeringBehavior", +"class": "GSTSeek", +"language": "GDScript", +"path": "res://src/behaviors/GSTSeek.gd" +}, { +"base": "GSTGroupBehavior", +"class": "GSTSeparation", +"language": "GDScript", +"path": "res://src/behaviors/GSTSeparation.gd" +}, { +"base": "GSTAgentLocation", +"class": "GSTSteeringAgent", +"language": "GDScript", +"path": "res://src/GSTSteeringAgent.gd" +}, { +"base": "Reference", +"class": "GSTSteeringBehavior", +"language": "GDScript", +"path": "res://src/GSTSteeringBehavior.gd" +}, { +"base": "Reference", +"class": "GSTTargetAcceleration", +"language": "GDScript", +"path": "res://src/GSTTargetAcceleration.gd" +}, { +"base": "Reference", +"class": "GSTUtils", +"language": "GDScript", +"path": "res://src/GSTUtils.gd" +} ] +_global_script_class_icons={ +"GSTAgentLocation": "", +"GSTArrive": "", +"GSTAvoidCollisions": "", +"GSTBlend": "", +"GSTCohesion": "", +"GSTEvade": "", +"GSTFace": "", +"GSTFlee": "", +"GSTFollowPath": "", +"GSTGroupBehavior": "", +"GSTInfiniteProximity": "", +"GSTLookWhereYouGo": "", +"GSTMatchOrientation": "", +"GSTPath": "", +"GSTPriority": "", +"GSTProximity": "", +"GSTPursue": "", +"GSTRadiusProximity": "", +"GSTSeek": "", +"GSTSeparation": "", +"GSTSteeringAgent": "", +"GSTSteeringBehavior": "", +"GSTTargetAcceleration": "", +"GSTUtils": "" +} + +[application] + +config/name="SteeringToolkit" +config/icon="res://icon.png" + +[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,"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,"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,"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,"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) + ] +} + +[rendering] + +environment/default_environment="res://default_env.tres" diff --git a/project/src/GSTAgentLocation.gd b/project/src/GSTAgentLocation.gd new file mode 100644 index 0000000..df804c5 --- /dev/null +++ b/project/src/GSTAgentLocation.gd @@ -0,0 +1,6 @@ +class_name GSTAgentLocation +# Data type to represent an agent with a location and an orientation + + +var position: = Vector3.ZERO +var orientation: = 0.0 diff --git a/project/src/GSTGroupBehavior.gd b/project/src/GSTGroupBehavior.gd new file mode 100644 index 0000000..5a38843 --- /dev/null +++ b/project/src/GSTGroupBehavior.gd @@ -0,0 +1,16 @@ +extends GSTSteeringBehavior +class_name GSTGroupBehavior +# Extended behavior that features a Proximity group for group-based behaviors. + + +var proximity: GSTProximity + +var _callback: = funcref(self, "report_neighbor") + + +func _init(agent: GSTSteeringAgent, proximity: GSTProximity).(agent) -> void: + self.proximity = proximity + + +func report_neighbor(neighbor: GSTSteeringAgent) -> bool: + return false diff --git a/project/src/GSTPath.gd b/project/src/GSTPath.gd new file mode 100644 index 0000000..4277025 --- /dev/null +++ b/project/src/GSTPath.gd @@ -0,0 +1,125 @@ +extends Reference +class_name GSTPath +# Represents a path made up of Vector3 waypoints, split into path segments for use by path +# following algorithms. + +# # Keeping it updated requires calling `create_path` to update the path. + + +var open: bool +var path_length: float + +var _segments: Array + +var _nearest_point_on_segment: Vector3 +var _nearest_point_on_path: Vector3 + + +func _init(waypoints: Array, is_open: = false) -> void: + self.is_open = is_open + create_path(waypoints) + _nearest_point_on_segment = waypoints[0] + _nearest_point_on_path = waypoints[0] + + +func create_path(waypoints: Array) -> void: + if not waypoints or waypoints.size() < 2: + printerr("Waypoints cannot be null and must contain at least two (2) waypoints.") + + _segments = [] + path_length = 0 + var current: Vector3 = _segments[0] + var previous: Vector3 + + for i in range(1, waypoints.size(), 1): + previous = current + if i < waypoints.size(): + current = waypoints[i] + elif open: + break + else: + current = waypoints[0] + var segment: = GSTSegment.new(previous, current) + path_length += segment.length + segment.cumulative_length = path_length + _segments.append(segment) + + +func calculate_distance(agent_current_position: Vector3, path_parameter: Dictionary) -> float: + var smallest_distance_squared: float = INF + var nearest_segment: GSTSegment + for i in range(_segments.size()): + var segment: GSTSegment = _segments[i] + var distance_squared: = _calculate_point_segment_distance_squared( + segment.begin, + segment.end, + agent_current_position) + + if distance_squared < smallest_distance_squared: + _nearest_point_on_path = _nearest_point_on_segment + smallest_distance_squared = distance_squared + nearest_segment = segment + path_parameter.segment_index = i + + var length_on_path: = ( + nearest_segment.cumulative_length - + _nearest_point_on_path.distance_to(nearest_segment.end)) + + path_parameter.distance = length_on_path + + return length_on_path + + +func calculate_target_position(param: Dictionary, target_distance: float) -> Vector3: + if open: + target_distance = clamp(target_distance, 0, path_length) + else: + if target_distance < 0: + target_distance = path_length + fmod(target_distance, path_length) + elif target_distance > path_length: + target_distance = fmod(target_distance, path_length) + + var desired_segment: GSTSegment + for i in range(_segments.size()): + var segment: GSTSegment = _segments[i] + if segment.cumulative_length >= target_distance: + desired_segment = segment + break + + var distance: = desired_segment.cumulative_length - target_distance + + return ( + (desired_segment.begin - desired_segment.end) * + (distance / desired_segment.length) + desired_segment.end) + + +func get_start_point() -> Vector3: + return _segments.front().begin + + +func get_end_point() -> Vector3: + return _segments.back().end + + +func _calculate_point_segment_distance_squared(start: Vector3, end: Vector3, position: Vector3) -> float: + _nearest_point_on_segment = start + var start_end: = end - start + var start_end_length_squared: = start_end.length_squared() + if start_end_length_squared != 0: + var t = (position - start).dot(start_end) / start_end_length_squared + _nearest_point_on_segment += start_end * clamp(t, 0, 1) + + return _nearest_point_on_segment.distance_squared_to(position) + + +class GSTSegment: + var begin: Vector3 + var end: Vector3 + var length: float + var cumulative_length: float + + + func _init(begin: Vector3, end: Vector3) -> void: + self.begin = begin + self.end = end + length = begin.distance_to(end) diff --git a/project/src/GSTSteeringAgent.gd b/project/src/GSTSteeringAgent.gd new file mode 100644 index 0000000..68c787a --- /dev/null +++ b/project/src/GSTSteeringAgent.gd @@ -0,0 +1,14 @@ +extends GSTAgentLocation +class_name GSTSteeringAgent +# Extended agent data type that adds velocity, speed, and size data + + +var zero_linear_speed_threshold: = 0.01 +var max_linear_speed: = 0.0 +var max_linear_acceleration: = 0.0 +var max_angular_speed: = 0.0 +var max_angular_acceleration: = 0.0 +var linear_velocity: = Vector3.ZERO +var angular_velocity: = 0.0 +var bounding_radius: = 0.0 +var tagged: = false diff --git a/project/src/GSTSteeringBehavior.gd b/project/src/GSTSteeringBehavior.gd new file mode 100644 index 0000000..961c49a --- /dev/null +++ b/project/src/GSTSteeringBehavior.gd @@ -0,0 +1,23 @@ +class_name GSTSteeringBehavior +# Base class to calculate how an AI agent steers itself. + + +var enabled: = true +var agent: GSTSteeringAgent + + +func _init(agent: GSTSteeringAgent) -> void: + self.agent = agent + + +func calculate_steering(acceleration: GSTTargetAcceleration) -> GSTTargetAcceleration: + if enabled: + return _calculate_steering(acceleration) + else: + acceleration.set_zero() + return acceleration + + +func _calculate_steering(acceleration: GSTTargetAcceleration) -> GSTTargetAcceleration: + acceleration.set_zero() + return acceleration diff --git a/project/src/GSTTargetAcceleration.gd b/project/src/GSTTargetAcceleration.gd new file mode 100644 index 0000000..a9caccf --- /dev/null +++ b/project/src/GSTTargetAcceleration.gd @@ -0,0 +1,26 @@ +class_name GSTTargetAcceleration +# A linear and angular amount of acceleration. + + +var linear: = Vector3.ZERO +var angular: = 0.0 + + +func set_zero() -> void: + linear.x = 0.0 + linear.y = 0.0 + linear.z = 0.0 + angular = 0.0 + + +func add_scaled_accel(accel: GSTTargetAcceleration, scalar: float) -> void: + linear += accel.linear * scalar + angular += accel.angular * scalar + + +func get_magnitude_squared() -> float: + return linear.length_squared() + angular * angular + + +func get_magnitude() -> float: + return sqrt(get_magnitude_squared()) diff --git a/project/src/GSTUtils.gd b/project/src/GSTUtils.gd new file mode 100644 index 0000000..b366ff5 --- /dev/null +++ b/project/src/GSTUtils.gd @@ -0,0 +1,10 @@ +class_name GSTUtils +# Useful math and utility functions to complement Godot's own. + + +static func clampedv3(vector: Vector3, limit: float) -> Vector3: + var length_squared: = vector.length_squared() + var limit_squared: = limit * limit + if length_squared > limit_squared: + vector *= sqrt(limit_squared / length_squared) + return vector diff --git a/project/src/behaviors/GSTArrive.gd b/project/src/behaviors/GSTArrive.gd new file mode 100644 index 0000000..eeb84c0 --- /dev/null +++ b/project/src/behaviors/GSTArrive.gd @@ -0,0 +1,40 @@ +extends GSTSteeringBehavior +class_name GSTArrive +# Calculates acceleration to take an agent to its target's location. +# The calculation will attempt to arrive with zero remaining velocity. + + +var target: GSTAgentLocation +var arrival_tolerance: float +var deceleration_radius: float +var time_to_reach: = 0.1 + + +func _init(agent: GSTSteeringAgent, target: GSTAgentLocation).(agent) -> void: + self.target = target + + +func _arrive(acceleration: GSTTargetAcceleration, target_position: Vector3) -> GSTTargetAcceleration: + var to_target: = target_position - agent.position + var distance: = to_target.length() + + if distance <= arrival_tolerance: + acceleration.set_zero() + else: + var desired_speed: = agent.max_linear_speed + + if distance <= deceleration_radius: + desired_speed *= distance / deceleration_radius + + var desired_velocity: = to_target * desired_speed/distance + + desired_velocity = (desired_velocity - agent.linear_velocity) * 1.0 / time_to_reach + + acceleration.linear = GSTUtils.clampedv3(desired_velocity, agent.max_linear_acceleration) + acceleration.angular = 0 + + return acceleration + + +func _calculate_steering(acceleration: GSTTargetAcceleration) -> GSTTargetAcceleration: + return _arrive(acceleration, target.position) diff --git a/project/src/behaviors/GSTAvoidCollisions.gd b/project/src/behaviors/GSTAvoidCollisions.gd new file mode 100644 index 0000000..d4ed8d2 --- /dev/null +++ b/project/src/behaviors/GSTAvoidCollisions.gd @@ -0,0 +1,65 @@ +extends GSTGroupBehavior +class_name GSTAvoidCollisions +# Behavior that steers the agent to avoid obstacles lying in its path, approximated by a sphere. + + +var first_neighbor: GSTSteeringAgent +var shortest_time: float +var first_minimum_separation: float +var first_distance: float +var first_relative_position: Vector3 +var first_relative_velocity: Vector3 + + +func _init(agent: GSTSteeringAgent, proximity: GSTProximity).(agent, proximity) -> void: + pass + + +func _calculate_steering(acceleration: GSTTargetAcceleration) -> GSTTargetAcceleration: + shortest_time = INF + first_neighbor = null + first_minimum_separation = 0 + first_distance = 0 + + var neighbor_count: = proximity.find_neighbors(_callback) + + if neighbor_count == 0 or not first_neighbor: + acceleration.set_zero() + else: + if( + first_minimum_separation <= 0 or + first_distance < agent.bounding_radius + first_neighbor.bounding_radius): + acceleration.linear = first_neighbor.position - agent.position + else: + acceleration.linear = first_relative_position + (first_relative_velocity * shortest_time) + acceleration.linear = acceleration.linear.normalized() * -agent.max_linear_acceleration + acceleration.angular = 0 + + return acceleration + + +func report_neighbor(neighbor: GSTSteeringAgent) -> bool: + var relative_position: = neighbor.position - agent.position + var relative_velocity: = neighbor.linear_velocity - agent.linear_velocity + var relative_speed_squared: = relative_velocity.length_squared() + + if relative_speed_squared == 0: + return false + else: + var time_to_collision = -relative_position.dot(relative_velocity) / relative_speed_squared + + if time_to_collision <= 0 or time_to_collision >= shortest_time: + return false + else: + var distance = relative_position.length() + var minimum_separation: float = distance - sqrt(relative_speed_squared) * time_to_collision + if minimum_separation > agent.bounding_radius + neighbor.bounding_radius: + return false + else: + shortest_time = time_to_collision + first_neighbor = neighbor + first_minimum_separation = minimum_separation + first_distance = distance + first_relative_position = relative_position + first_relative_velocity = relative_velocity + return true diff --git a/project/src/behaviors/GSTBlend.gd b/project/src/behaviors/GSTBlend.gd new file mode 100644 index 0000000..67f5899 --- /dev/null +++ b/project/src/behaviors/GSTBlend.gd @@ -0,0 +1,44 @@ +extends GSTSteeringBehavior +class_name GSTBlend +# Blends multiple steering behaviors into one, and returns acceleration combining all of them. + +# # Each behavior is associated with a weight - a modifier by which the result will be multiplied by, +# then added to a total acceleration. + +# # Each behavior is stored internally as a `Dictionary` with a `behavior` key with a value of type +# `GSTSteeringBehavior` and a `weight` key with a value of type float. + + +var _behaviors: = [] +var _accel: = GSTTargetAcceleration.new() + + +func _init(agent: GSTSteeringAgent).(agent) -> void: + pass + + +func add(behavior: GSTSteeringBehavior, weight: float) -> void: + behavior.agent = agent + _behaviors.append({behavior = behavior, weight = weight}) + + +func get_behavior_at(index: int) -> Dictionary: + if _behaviors.size() > index: + return _behaviors[index] + printerr("Tried to get index " + str(index) + " in array of size " + str(_behaviors.size())) + return {} + + +func _calculate_steering(blended_accel: GSTTargetAcceleration) -> GSTTargetAcceleration: + blended_accel.set_zero() + + for i in range(_behaviors.size()): + var bw: Dictionary = _behaviors[i] + bw.behavior.calculate_steering(_accel) + + blended_accel.add_scaled_accel(_accel, bw.weight) + + blended_accel.linear = GSTUtils.clampedv3(blended_accel.linear, agent.max_linear_acceleration) + blended_accel.angular = min(blended_accel.angular, agent.max_angular_acceleration) + + return blended_accel diff --git a/project/src/behaviors/GSTCohesion.gd b/project/src/behaviors/GSTCohesion.gd new file mode 100644 index 0000000..12fe0ac --- /dev/null +++ b/project/src/behaviors/GSTCohesion.gd @@ -0,0 +1,26 @@ +extends GSTGroupBehavior +class_name GSTCohesion +# Group behavior that produces linear acceleration that attempts to move the agent towards the +# center of mass of the agents in the area defined by the defined Proximity. + + +var center_of_mass: Vector3 + + +func _init(agent: GSTSteeringAgent, proximity: GSTProximity).(agent, proximity) -> void: + pass + + +func _calculate_steering(acceleration: GSTTargetAcceleration) -> GSTTargetAcceleration: + acceleration.set_zero() + center_of_mass = Vector3.ZERO + var neighbor_count = proximity.find_neighbors(_callback) + if neighbor_count > 0: + center_of_mass *= 1.0 / neighbor_count + acceleration.linear = (center_of_mass - agent.position).normalized() * agent.max_linear_acceleration + return acceleration + + +func report_neighbor(neighbor: GSTSteeringAgent) -> bool: + center_of_mass += neighbor.position + return true diff --git a/project/src/behaviors/GSTEvade.gd b/project/src/behaviors/GSTEvade.gd new file mode 100644 index 0000000..ce89767 --- /dev/null +++ b/project/src/behaviors/GSTEvade.gd @@ -0,0 +1,16 @@ +extends GSTPursue +class_name GSTEvade +# Calculates acceleration to take an agent away from where a target agent will be. + +# # The `max_predict_time` variable represents how far ahead to calculate the intersection point. + + +func _init( + agent: GSTSteeringAgent, + target: GSTSteeringAgent, + max_predict_time: = 1.0).(agent, target, max_predict_time): + pass + + +func _get_modified_acceleration() -> float: + return -agent.max_linear_acceleration diff --git a/project/src/behaviors/GSTFace.gd b/project/src/behaviors/GSTFace.gd new file mode 100644 index 0000000..7a5c3dd --- /dev/null +++ b/project/src/behaviors/GSTFace.gd @@ -0,0 +1,24 @@ +extends GSTMatchOrientation +class_name GSTFace +# Calculates angular acceleration to rotate a target to face its target's position. +# The acceleration will attempt to arrive with zero remaining angular velocity. + + +func _init(agent: GSTSteeringAgent, target: GSTAgentLocation).(agent, target) -> void: + pass + + +func _face(acceleration: GSTTargetAcceleration, target_position: Vector3) -> GSTTargetAcceleration: + var to_target: = target_position - agent.position + var distance_squared: = to_target.length_squared() + + if distance_squared < agent.zero_linear_speed_threshold: + acceleration.set_zero() + return acceleration + else: + var orientation = atan2(to_target.x, -to_target.y) + return _match_orientation(acceleration, orientation) + + +func _calculate_steering(acceleration: GSTTargetAcceleration) -> GSTTargetAcceleration: + return _face(acceleration, target.position) diff --git a/project/src/behaviors/GSTFlee.gd b/project/src/behaviors/GSTFlee.gd new file mode 100644 index 0000000..021ed24 --- /dev/null +++ b/project/src/behaviors/GSTFlee.gd @@ -0,0 +1,15 @@ +extends GSTSeek +class_name GSTFlee +# Calculates acceleration to take an agent directly away from a target agent. + + +func _init(agent: GSTSteeringAgent, target: GSTAgentLocation).(agent, target) -> void: + pass + + +func _calculate_steering(acceleration: GSTTargetAcceleration) -> GSTTargetAcceleration: + acceleration.linear = ( + (agent.position - target.position).normalized() * agent.max_linear_acceleration) + acceleration.angular = 0 + + return acceleration diff --git a/project/src/behaviors/GSTFollowPath.gd b/project/src/behaviors/GSTFollowPath.gd new file mode 100644 index 0000000..49699a3 --- /dev/null +++ b/project/src/behaviors/GSTFollowPath.gd @@ -0,0 +1,47 @@ +extends GSTArrive +class_name GSTFollowPath +# Produces a linear acceleration that moves the agent along the specified path. + + +var path: GSTPath +var path_offset: = 0.0 + +var path_param: = {} + +var arrive_enabled: = true +var prediction_time: = 0.0 + + +func _init( + agent: GSTSteeringAgent, + path: GSTPath, + path_offset: = 0.0, + prediction_time: = 0.0).(agent, null) -> void: + self.path = path + self.path_offset = path_offset + self.prediction_time = prediction_time + + +func _calculate_steering(acceleration: GSTTargetAcceleration) -> GSTTargetAcceleration: + var location: = ( + agent.position if prediction_time == 0 + else agent.position + (agent.linear_velocity * prediction_time)) + + var distance: = path.calculate_distance(location, path_param) + var target_distance: = distance + path_offset + + var target_position: = path.calculate_target_position(path_param, target_distance) + + if arrive_enabled and path.is_open: + if path_offset >= 0: + if target_distance > path.length - deceleration_radius: + return _arrive(acceleration, target_position) + else: + if target_distance < deceleration_radius: + return _arrive(acceleration, target_position) + + acceleration.linear = (target_position - agent.position).normalized() + acceleration.linear *= agent.max_linear_acceleration + acceleration.angular = 0 + + return acceleration diff --git a/project/src/behaviors/GSTLookWhereYouGo.gd b/project/src/behaviors/GSTLookWhereYouGo.gd new file mode 100644 index 0000000..058becf --- /dev/null +++ b/project/src/behaviors/GSTLookWhereYouGo.gd @@ -0,0 +1,16 @@ +extends GSTMatchOrientation +class_name GSTLookWhereYouGo +# Calculates an angular acceleration to match an agent's orientation to its direction of travel. + + +func _init(agent: GSTSteeringAgent).(agent, null) -> void: + pass + + +func _calculate_steering(accel: GSTTargetAcceleration) -> GSTTargetAcceleration: + if agent.linear_velocity.length_squared() < agent.zero_linear_speed_threshold: + accel.set_zero() + return accel + else: + var orientation: = atan2(agent.linear_velocity.x, -agent.linear_velocity.y) + return _match_orientation(accel, orientation) diff --git a/project/src/behaviors/GSTMatchOrientation.gd b/project/src/behaviors/GSTMatchOrientation.gd new file mode 100644 index 0000000..8185dc9 --- /dev/null +++ b/project/src/behaviors/GSTMatchOrientation.gd @@ -0,0 +1,44 @@ +extends GSTSteeringBehavior +class_name GSTMatchOrientation +# Calculates an angular acceleration to match an agent's orientation to its target's. +# The calculation will attempt to arrive with zero remaining angular velocity. + + +var target: GSTAgentLocation +var alignment_tolerance: float +var deceleration_radius: float +var time_to_reach: float = 0.1 + + +func _init(agent: GSTSteeringAgent, target: GSTAgentLocation).(agent) -> void: + self.target = target + + +func _match_orientation(acceleration: GSTTargetAcceleration, desired_orientation: float) -> GSTTargetAcceleration: + var rotation: = wrapf(desired_orientation - agent.orientation, -PI, PI) + + var rotation_size: = abs(rotation) + + if rotation_size <= alignment_tolerance: + acceleration.set_zero() + else: + var desired_rotation: = agent.max_angular_speed + + if rotation_size <= deceleration_radius: + desired_rotation *= rotation_size / deceleration_radius + + desired_rotation *= rotation / rotation_size + + acceleration.angular = (desired_rotation - agent.angular_velocity) / time_to_reach + + var limited_acceleration: = abs(acceleration.angular) + if limited_acceleration > agent.max_angular_acceleration: + acceleration.angular *= agent.max_angular_acceleration / limited_acceleration + + acceleration.linear = Vector3.ZERO + + return acceleration + + +func _calculate_steering(acceleration: GSTTargetAcceleration) -> GSTTargetAcceleration: + return _match_orientation(acceleration, target.orientation) diff --git a/project/src/behaviors/GSTPriority.gd b/project/src/behaviors/GSTPriority.gd new file mode 100644 index 0000000..4cb84df --- /dev/null +++ b/project/src/behaviors/GSTPriority.gd @@ -0,0 +1,46 @@ +extends GSTSteeringBehavior +class_name GSTPriority +# Contains multiple steering behaviors and returns only the result of the first that has a non-zero + # acceleration. + + +var _behaviors: = [] + +var last_selected_index: int +var threshold_for_zero: float + + +func _init(agent: GSTSteeringAgent, threshold_for_zero: = 0.001).(agent) -> void: + self.threshold_for_zero = threshold_for_zero + + +func add(behavior: GSTSteeringBehavior) -> void: + _behaviors.append(behavior) + + +func get_behavior_at(index: int) -> GSTSteeringBehavior: + if _behaviors.size() > index: + return _behaviors[index] + printerr("Tried to get index " + str(index) + " in array of size " + str(_behaviors.size())) + return null + + +func _calculate_steering(accel: GSTTargetAcceleration) -> GSTTargetAcceleration: + var threshold_squared: = threshold_for_zero * threshold_for_zero + + last_selected_index = -1 + + var size: = _behaviors.size() + + if size > 0: + for i in range(size): + last_selected_index = i + var behavior: GSTSteeringBehavior = _behaviors[i] + behavior.calculate_steering(accel) + + if accel.get_magnitude_squared() > threshold_squared: + break + else: + accel.set_zero() + + return accel diff --git a/project/src/behaviors/GSTPursue.gd b/project/src/behaviors/GSTPursue.gd new file mode 100644 index 0000000..84b2efd --- /dev/null +++ b/project/src/behaviors/GSTPursue.gd @@ -0,0 +1,42 @@ +extends GSTSteeringBehavior +class_name GSTPursue +# Calculates acceleration to take an agent to intersect with where a target agent will be. + +# # The `max_predict_time` variable represents how far ahead to calculate the intersection point. + + +var target: GSTSteeringAgent +var max_predict_time: float + + +func _init( + agent: GSTSteeringAgent, + target: GSTSteeringAgent, + max_predict_time: = 1.0).(agent) -> void: + self.target = target + self.max_predict_time = max_predict_time + + +func _calculate_steering(acceleration: GSTTargetAcceleration) -> GSTTargetAcceleration: + var target_position: = target.position + var distance_squared: = (target_position - agent.position).length_squared() + + var speed_squared: = agent.linear_velocity.length_squared() + var predict_time: = max_predict_time + + if speed_squared > 0: + var predict_time_squared: = distance_squared / speed_squared + if predict_time_squared < max_predict_time * max_predict_time: + predict_time = sqrt(predict_time_squared) + + acceleration.linear = (( + target_position + (target.linear_velocity * predict_time))-agent.position).normalized() + acceleration.linear *= _get_modified_acceleration() + + acceleration.angular = 0 + + return acceleration + + +func _get_modified_acceleration() -> float: + return agent.max_linear_acceleration diff --git a/project/src/behaviors/GSTSeek.gd b/project/src/behaviors/GSTSeek.gd new file mode 100644 index 0000000..7d9ee30 --- /dev/null +++ b/project/src/behaviors/GSTSeek.gd @@ -0,0 +1,18 @@ +extends GSTSteeringBehavior +class_name GSTSeek +# Calculates acceleration to take an agent to a target agent's position as directly as possible + + +var target: GSTAgentLocation + + +func _init(agent: GSTSteeringAgent, target: GSTAgentLocation).(agent) -> void: + self.target = target + + +func _calculate_steering(acceleration: GSTTargetAcceleration) -> GSTTargetAcceleration: + acceleration.linear = ( + (target.position - agent.position).normalized() * agent.max_linear_acceleration) + acceleration.angular = 0 + + return acceleration diff --git a/project/src/behaviors/GSTSeparation.gd b/project/src/behaviors/GSTSeparation.gd new file mode 100644 index 0000000..9d9a573 --- /dev/null +++ b/project/src/behaviors/GSTSeparation.gd @@ -0,0 +1,39 @@ +extends GSTGroupBehavior +class_name GSTSeparation +# Group behavior that produces acceleration repelling from the other neighbors that are in the +# immediate area defined by the given `GSTProximity`. + +# # The produced acceleration is an average of all agents under consideration, multiplied by a +# # strength decreasing by the inverse square law in relation to distance, and accumulated. +# # In effect, all neighbors produce a single repelling force. + + +var decay_coefficient: = 1.0 + +var acceleration: GSTTargetAcceleration + + +func _init(agent: GSTSteeringAgent, proximity: GSTProximity).(agent, proximity) -> void: + pass + + +func _calculate_steering(acceleration: GSTTargetAcceleration) -> GSTTargetAcceleration: + acceleration.set_zero() + self.acceleration = acceleration + proximity.find_neighbors(_callback) + return acceleration + + +func report_neighbor(neighbor: GSTSteeringAgent) -> bool: + var to_agent: = agent.position - neighbor.position + + var distance_squared: = to_agent.length_squared() + var max_acceleration: = agent.max_linear_acceleration + + var strength: = decay_coefficient / distance_squared + if strength > max_acceleration: + strength = max_acceleration + + acceleration.linear += to_agent * (strength / sqrt(distance_squared)) + + return true diff --git a/project/src/proximities/GSTInfiniteProximity.gd b/project/src/proximities/GSTInfiniteProximity.gd new file mode 100644 index 0000000..0dd9b17 --- /dev/null +++ b/project/src/proximities/GSTInfiniteProximity.gd @@ -0,0 +1,20 @@ +extends GSTProximity +class_name GSTInfiniteProximity +# Determines any agent that is in the specified list as being neighbors with the owner agent. + + +func _init(agent: GSTSteeringAgent, agents: Array).(agent, agents) -> void: + pass + + +func find_neighbors(callback: FuncRef) -> int: + var neighbor_count: = 0 + var agent_count: = agents.size() + for i in range(agent_count): + var current_agent: = agents[i] as GSTSteeringAgent + + if current_agent != agent: + if callback.call_func(current_agent): + neighbor_count += 1 + + return neighbor_count diff --git a/project/src/proximities/GSTProximity.gd b/project/src/proximities/GSTProximity.gd new file mode 100644 index 0000000..f746410 --- /dev/null +++ b/project/src/proximities/GSTProximity.gd @@ -0,0 +1,17 @@ +extends Reference +class_name GSTProximity +# Defines a way to determine any agent that is in the specified list as being neighbors with the +# owner agent. + + +var agent: GSTSteeringAgent +var agents: = [] + + +func _init(agent: GSTSteeringAgent, agents: Array) -> void: + self.agent = agent + self.agents = agents + + +func find_neighbors(callback: FuncRef) -> int: + return 0 diff --git a/project/src/proximities/GSTRadiusProximity.gd b/project/src/proximities/GSTRadiusProximity.gd new file mode 100644 index 0000000..300fd11 --- /dev/null +++ b/project/src/proximities/GSTRadiusProximity.gd @@ -0,0 +1,51 @@ +extends GSTProximity +class_name GSTRadiusProximity +# Determines any agent that is in the specified list as being neighbors with the owner agent if +# they lie within the specified radius. + + +var radius: = 0.0 + +var _last_frame: = 0 +var _scene_tree: SceneTree + + +func _init(agent: GSTSteeringAgent, agents: Array, radius: float).(agent, agents) -> void: + self.radius = radius + _scene_tree = Engine.get_main_loop() + + +func find_neighbors(callback: FuncRef) -> int: + var agent_count: = agents.size() + var neighbor_count: = 0 + + var current_frame: = _scene_tree.get_frame() if _scene_tree else -_last_frame + if current_frame != _last_frame: + _last_frame = current_frame + + var owner_position: = agent.position + + for i in range(agent_count): + var current_agent: = agents[i] as GSTSteeringAgent + + if current_agent != agent: + var distance_squared: = owner_position.distance_squared_to(current_agent.position) + + var range_to: = radius + current_agent.bounding_radius + + if distance_squared < range_to * range_to: + if callback.call_func(current_agent): + current_agent.tagged = true + neighbor_count += 1 + continue + + current_agent.tagged = false + else: + for i in range(agent_count): + var current_agent = agents[i] as GSTSteeringAgent + + if current_agent != agent and current_agent.tagged: + if callback.call_func(current_agent): + neighbor_count += 1 + + return neighbor_count