commit bbd79270869d8fb0f2bc27b87c32f296eadd2f74 Author: Yusuf Bera Ertan Date: Mon Aug 19 16:53:54 2024 +0300 chore: init diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..8ad74f7 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# Normalize EOL for all files that Git considers text files. +* text=auto eol=lf diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0af181c --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +# Godot 4+ specific ignores +.godot/ +/android/ diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..1b1e6e2 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 yusdacra + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..0f33ea2 --- /dev/null +++ b/README.md @@ -0,0 +1,17 @@ +# boid_2d + +Addon for Godot that adds a 2D node for simulating boids / flocking. + +![boids](./resources/boids.gif) + +## Usage + +Clone the repository and copy over the addon. +Make an inherited scene from `boid.tscn`, add a `Sprite2D` (or whatever visuals you have) and instantiate and spawn many. +Checkout the examples on how to use it more. + +## TODO + +- [ ] fix weird spasming behaviour +- [ ] improve collision (dont only bounce, maybe follow wall in some conditions etc.) +- [ ] improve performance (BoidManager autoload that tracks and manages every boid?) diff --git a/addons/boid_2d/boid.gd b/addons/boid_2d/boid.gd new file mode 100644 index 0000000..1c8d3b5 --- /dev/null +++ b/addons/boid_2d/boid.gd @@ -0,0 +1,121 @@ +extends Area2D +class_name Boid + + +## sets the `RayCast2D` used to detect walls. +@export var wallcast: RayCast2D +## sets the `Area2D` used for vision (seeing other boids). +@export var vision: Area2D +## sets the rotate timer, allowing boids to perform random rotations based on the timer's timeout signal. +@export var rotate_timer: Timer + +@export_group("properties") +## controls the target (max) speed. +@export var target_speed := 6.0 +## controls how much other boids affect this boid. +## higher values will make them more dispersed. +@export var steer_away_factor := 40 +## controls whether or not to run collisions before running boid calculations. +## enabling this can help reduce boids escaping colliders, especially if they are following something. +@export var collide_first := false + +@export_group("follow") +## controls which node to try and follow, if any +@export var follow_point: Node2D +## controls the radius at which the boid will target, instead of the target directly +@export var follow_radius := 100.0 + +var last_follow_pos: Vector2 = Vector2.ZERO +var follow_target: Vector2 +var speed := target_speed +var vel := Vector2.ZERO +var boidsSeen: Dictionary = {} + + +func _ready() -> void: + assert(wallcast, "boid invalid: wallcast (RayCast3D) not assigned") + assert(vision, "boid invalid: vision (Area2D) not assigned") + if rotate_timer: + rotate_timer.timeout.connect(_on_rotate_timer_timeout) + + +func _physics_process(delta: float) -> void: + if collide_first: + _process_collision() + _process_boids() + else: + _process_boids() + _process_collision() + # move boid + var vel_dir := vel.normalized() + # fix if a boid stops by getting seperated and its vel being cancelled at the same time + if vel_dir.is_zero_approx(): vel_dir = Vector2.RIGHT + vel = vel_dir * speed + global_position += vel + # rotate boid + global_rotation = atan2(vel_dir.y, vel_dir.x) + + +func _process_boids() -> void: + var numOfBoids := boidsSeen.size() + var avgVel := Vector2.ZERO + var avgPos := Vector2.ZERO + var steerAway := Vector2.ZERO + if numOfBoids > 0: + for boid: Boid in boidsSeen.values(): + avgVel += boid.vel; avgPos += boid.global_position + var dist := boid.global_position - global_position + steerAway -= dist * (steer_away_factor / dist.length()) + + # apply follow point vel + if follow_point: + var dist_to_follow := global_position.distance_to(follow_point.global_position) + if global_position.distance_to(follow_target) < 10.0 or dist_to_follow > follow_radius: + _calc_follow_target() + # slow down speed when nearing target + speed = maxf(0.0, lerpf(target_speed, 0.0, follow_radius / dist_to_follow)) + var target_vel := (follow_point.global_position - last_follow_pos) * Engine.physics_ticks_per_second + avgVel += target_vel + avgPos += follow_target + var dist := follow_target - global_position + steerAway -= dist * ((steer_away_factor + follow_radius) / dist.length()) + numOfBoids += 1 + last_follow_pos = follow_point.global_position + + if numOfBoids > 0: + avgVel /= numOfBoids + vel += (avgVel - vel) / 2 + + avgPos /= numOfBoids + vel += avgPos - global_position + + steerAway /= numOfBoids + vel += steerAway + + +func _calc_follow_target() -> void: + var follow_vec := follow_point.global_position - global_position + var target_length := follow_vec.length() + follow_radius + follow_target = global_position + follow_vec.normalized() * target_length + + +func _process_collision() -> void: + wallcast.force_raycast_update() + if not wallcast.is_colliding(): return + + var col_normal: Vector2 = wallcast.get_collision_normal() + vel = vel.bounce(col_normal) + + +func _on_vision_area_entered(area: Area2D) -> void: + if area == self: return + boidsSeen[area.get_instance_id()] = area + + +func _on_vision_area_exited(area: Area2D) -> void: + boidsSeen.erase(area.get_instance_id()) + + +func _on_rotate_timer_timeout() -> void: + vel -= Vector2(randf(), randf()) * speed + rotate_timer.start() diff --git a/addons/boid_2d/boid.tscn b/addons/boid_2d/boid.tscn new file mode 100644 index 0000000..0217c0e --- /dev/null +++ b/addons/boid_2d/boid.tscn @@ -0,0 +1,34 @@ +[gd_scene load_steps=4 format=3 uid="uid://bq7s2yf0fohes"] + +[ext_resource type="Script" path="res://addons/boid_2d/boid.gd" id="1_xwhwb"] + +[sub_resource type="RectangleShape2D" id="RectangleShape2D_ackok"] +size = Vector2(32, 16) + +[sub_resource type="RectangleShape2D" id="RectangleShape2D_ipkm3"] +size = Vector2(60, 60) + +[node name="Boid" type="Area2D" node_paths=PackedStringArray("wallcast", "vision")] +collision_mask = 0 +monitoring = false +script = ExtResource("1_xwhwb") +wallcast = NodePath("WallCast") +vision = NodePath("Vision") + +[node name="CollisionShape2D" type="CollisionShape2D" parent="."] +shape = SubResource("RectangleShape2D_ackok") + +[node name="WallCast" type="RayCast2D" parent="."] +enabled = false +target_position = Vector2(50, 0) + +[node name="Vision" type="Area2D" parent="."] +collision_layer = 0 +monitorable = false + +[node name="CollisionShape2D" type="CollisionShape2D" parent="Vision"] +position = Vector2(42, 0) +shape = SubResource("RectangleShape2D_ipkm3") + +[connection signal="area_entered" from="Vision" to="." method="_on_vision_area_entered"] +[connection signal="area_exited" from="Vision" to="." method="_on_vision_area_exited"] diff --git a/addons/boid_2d/boid_2d.gd b/addons/boid_2d/boid_2d.gd new file mode 100644 index 0000000..1909b35 --- /dev/null +++ b/addons/boid_2d/boid_2d.gd @@ -0,0 +1,10 @@ +@tool +extends EditorPlugin + + +func _enter_tree() -> void: + add_custom_type("Boid2D", "Area2D", preload("boid.gd"), preload("boid_2d.svg")) + + +func _exit_tree() -> void: + remove_custom_type("Boid2D") diff --git a/addons/boid_2d/boid_2d.svg b/addons/boid_2d/boid_2d.svg new file mode 100644 index 0000000..ca169d2 --- /dev/null +++ b/addons/boid_2d/boid_2d.svg @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + diff --git a/addons/boid_2d/boid_2d.svg.import b/addons/boid_2d/boid_2d.svg.import new file mode 100644 index 0000000..a686f80 --- /dev/null +++ b/addons/boid_2d/boid_2d.svg.import @@ -0,0 +1,38 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://c32akbxu1rkj8" +path="res://.godot/imported/boid_2d.svg-172f46790795ecd4cef523288fe60368.ctex" +metadata={ +"has_editor_variant": true, +"vram_texture": false +} + +[deps] + +source_file="res://addons/boid_2d/boid_2d.svg" +dest_files=["res://.godot/imported/boid_2d.svg-172f46790795ecd4cef523288fe60368.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=1.0 +editor/scale_with_editor_scale=true +editor/convert_colors_with_editor_theme=true diff --git a/addons/boid_2d/plugin.cfg b/addons/boid_2d/plugin.cfg new file mode 100644 index 0000000..e80610e --- /dev/null +++ b/addons/boid_2d/plugin.cfg @@ -0,0 +1,7 @@ +[plugin] + +name="Boid2D" +description="Addon for implementing boids / flocking in Godot." +author="yusdacra" +version="0.1" +script="boid_2d.gd" diff --git a/examples/boid_2d/example_boid.svg b/examples/boid_2d/example_boid.svg new file mode 100644 index 0000000..ffc25c7 --- /dev/null +++ b/examples/boid_2d/example_boid.svg @@ -0,0 +1,65 @@ + + + + + + + + + + + + diff --git a/examples/boid_2d/example_boid.svg.import b/examples/boid_2d/example_boid.svg.import new file mode 100644 index 0000000..845b073 --- /dev/null +++ b/examples/boid_2d/example_boid.svg.import @@ -0,0 +1,37 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://rk5u1wthr0n0" +path="res://.godot/imported/example_boid.svg-ebae3589d3b59182aead052ab0bb5c16.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://examples/boid_2d/example_boid.svg" +dest_files=["res://.godot/imported/example_boid.svg-ebae3589d3b59182aead052ab0bb5c16.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=6.0 +editor/scale_with_editor_scale=false +editor/convert_colors_with_editor_theme=false diff --git a/examples/boid_2d/example_boid.tscn b/examples/boid_2d/example_boid.tscn new file mode 100644 index 0000000..ac55e9d --- /dev/null +++ b/examples/boid_2d/example_boid.tscn @@ -0,0 +1,15 @@ +[gd_scene load_steps=3 format=3 uid="uid://bcyffgnn2ahl3"] + +[ext_resource type="PackedScene" uid="uid://bq7s2yf0fohes" path="res://addons/boid_2d/boid.tscn" id="1_825c0"] +[ext_resource type="Texture2D" uid="uid://rk5u1wthr0n0" path="res://examples/boid_2d/example_boid.svg" id="2_qfbgc"] + +[node name="Boid" instance=ExtResource("1_825c0")] +collision_layer = 2 + +[node name="Vision" parent="." index="2"] +collision_mask = 2 + +[node name="Sprite2D" type="Sprite2D" parent="." index="3"] +position = Vector2(-2.17226e-06, 2.38419e-07) +scale = Vector2(0.111111, 0.111111) +texture = ExtResource("2_qfbgc") diff --git a/examples/boid_2d/follow/example.gd b/examples/boid_2d/follow/example.gd new file mode 100644 index 0000000..c6c5cd2 --- /dev/null +++ b/examples/boid_2d/follow/example.gd @@ -0,0 +1,18 @@ +extends Node2D + + +func _ready() -> void: + for i in 40: spawnBoid() + + +func _process(delta: float) -> void: + $Path2D/PathFollow2D.progress_ratio += delta * 0.1 + + +func spawnBoid() -> void: + var boid: Boid = preload("../example_boid.tscn").instantiate() + var screensize := get_viewport_rect().size + boid.modulate = Color(randf(), randf(), randf(), 1) + boid.global_position = Vector2((randf_range(200, screensize.x - 200)), (randf_range(200, screensize.y - 200))) + boid.follow_point = $Path2D/PathFollow2D + add_child(boid) diff --git a/examples/boid_2d/follow/example.tscn b/examples/boid_2d/follow/example.tscn new file mode 100644 index 0000000..14a1d93 --- /dev/null +++ b/examples/boid_2d/follow/example.tscn @@ -0,0 +1,19 @@ +[gd_scene load_steps=3 format=3 uid="uid://ckc0dhvrksfh4"] + +[ext_resource type="Script" path="res://examples/boid_2d/follow/example.gd" id="1_cb4mx"] + +[sub_resource type="Curve2D" id="Curve2D_ncwi0"] +_data = { +"points": PackedVector2Array(69.2957, 57.9564, -69.2957, -57.9564, 1117, 34, 79.375, -118.433, -79.375, 118.433, 34, 28, -61.7361, -114.653, 61.7361, 114.653, 22, 624, -42.8373, 69.2957, 42.8373, -69.2957, 1126, 622, 66.7758, 73.0754, -66.7758, -73.0754, 1117, 34) +} +point_count = 5 + +[node name="Example" type="Node2D"] +script = ExtResource("1_cb4mx") + +[node name="Path2D" type="Path2D" parent="."] +curve = SubResource("Curve2D_ncwi0") + +[node name="PathFollow2D" type="PathFollow2D" parent="Path2D"] +position = Vector2(1117, 34) +rotation = -2.44507 diff --git a/examples/boid_2d/simple/example.gd b/examples/boid_2d/simple/example.gd new file mode 100644 index 0000000..fe1c074 --- /dev/null +++ b/examples/boid_2d/simple/example.gd @@ -0,0 +1,13 @@ +extends Node2D + + +func _ready() -> void: + for i in 100: spawnBoid() + + +func spawnBoid() -> void: + var boid: Boid = preload("../example_boid.tscn").instantiate() + var screensize := get_viewport_rect().size + boid.modulate = Color(randf(), randf(), randf(), 1) + boid.global_position = Vector2((randf_range(200, screensize.x - 200)), (randf_range(200, screensize.y - 200))) + add_child(boid) diff --git a/examples/boid_2d/simple/example.tscn b/examples/boid_2d/simple/example.tscn new file mode 100644 index 0000000..3652ef8 --- /dev/null +++ b/examples/boid_2d/simple/example.tscn @@ -0,0 +1,12 @@ +[gd_scene load_steps=2 format=3 uid="uid://op0qicvpbjt6"] + +[ext_resource type="Script" path="res://examples/boid_2d/simple/example.gd" id="1_3gcrf"] + +[node name="Example" type="Node2D"] +script = ExtResource("1_3gcrf") + +[node name="StaticBody2D" type="StaticBody2D" parent="."] +collision_priority = 4.0 + +[node name="CollisionPolygon2D" type="CollisionPolygon2D" parent="StaticBody2D"] +polygon = PackedVector2Array(1152, 0, 1152, 648, 0, 648, 0, 0, 1088, 0, 1088, 64, 64, 64, 64, 576, 1088, 576, 1088, 0) diff --git a/project.godot b/project.godot new file mode 100644 index 0000000..8372d9d --- /dev/null +++ b/project.godot @@ -0,0 +1,24 @@ +; 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=5 + +[application] + +config/name="Boid Addon" +run/main_scene="res://examples/boid_2d/simple/example.tscn" +config/features=PackedStringArray("4.3", "GL Compatibility") + +[editor_plugins] + +enabled=PackedStringArray("res://addons/boid_2d/plugin.cfg") + +[rendering] + +renderer/rendering_method="gl_compatibility" +renderer/rendering_method.mobile="gl_compatibility" diff --git a/resources/boid_2d.png b/resources/boid_2d.png new file mode 100644 index 0000000..5176eb7 Binary files /dev/null and b/resources/boid_2d.png differ diff --git a/resources/boid_2d.png.import b/resources/boid_2d.png.import new file mode 100644 index 0000000..a446fe4 --- /dev/null +++ b/resources/boid_2d.png.import @@ -0,0 +1,34 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://cp6l2f1oac4ce" +path="res://.godot/imported/boid_2d.png-b56b7a427c09dbbdb1e601cb745d4254.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://resources/boid_2d.png" +dest_files=["res://.godot/imported/boid_2d.png-b56b7a427c09dbbdb1e601cb745d4254.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/resources/boids.gif b/resources/boids.gif new file mode 100644 index 0000000..e93e4e5 Binary files /dev/null and b/resources/boids.gif differ