From a26e1fa993fe36affee1931e78bec5a426fbb57b Mon Sep 17 00:00:00 2001 From: dusk Date: Mon, 26 Aug 2024 11:55:38 +0300 Subject: [PATCH] feat: rewrite in a more performant way, support 3d --- addons/boid_2d/boid.gd | 121 -------------- addons/boid_2d/boid.tscn | 34 ---- addons/boid_2d/boid_2d.gd | 10 -- addons/boid_2d/plugin.cfg | 7 - addons/boids/boid_2d/boid_2d.gd | 24 +++ addons/boids/boid_2d/boid_2d.svg | 68 ++++++++ addons/{ => boids}/boid_2d/boid_2d.svg.import | 6 +- addons/boids/boid_3d/boid_3d.gd | 23 +++ addons/boids/boid_3d/boid_3d.svg | 68 ++++++++ addons/boids/boid_3d/boid_3d.svg.import | 38 +++++ addons/boids/boid_manager.gd | 147 ++++++++++++++++++ .../boids/boid_properties/boid_properties.gd | 17 ++ .../boids/boid_properties/boid_properties.svg | 31 ++-- .../boid_properties.svg.import | 38 +++++ addons/boids/boids.gd | 18 +++ addons/boids/flock/flock.gd | 34 ++++ .../boid_2d.svg => boids/flock/flock.svg} | 14 +- addons/boids/flock/flock.svg.import | 38 +++++ addons/boids/plugin.cfg | 7 + examples/boid_2d/example_boid.tscn | 15 -- examples/boid_2d/simple/example.tscn | 12 -- examples/boids/2d/example_boid.svg | 69 ++++++++ .../2d}/example_boid.svg.import | 8 +- examples/boids/2d/example_boid.tscn | 34 ++++ .../{boid_2d => boids/2d}/follow/example.gd | 8 +- .../{boid_2d => boids/2d}/follow/example.tscn | 9 +- .../{boid_2d => boids/2d}/simple/example.gd | 11 +- examples/boids/2d/simple/example.tscn | 22 +++ project.godot | 21 ++- 29 files changed, 709 insertions(+), 243 deletions(-) delete mode 100644 addons/boid_2d/boid.gd delete mode 100644 addons/boid_2d/boid.tscn delete mode 100644 addons/boid_2d/boid_2d.gd delete mode 100644 addons/boid_2d/plugin.cfg create mode 100644 addons/boids/boid_2d/boid_2d.gd create mode 100644 addons/boids/boid_2d/boid_2d.svg rename addons/{ => boids}/boid_2d/boid_2d.svg.import (75%) create mode 100644 addons/boids/boid_3d/boid_3d.gd create mode 100644 addons/boids/boid_3d/boid_3d.svg create mode 100644 addons/boids/boid_3d/boid_3d.svg.import create mode 100644 addons/boids/boid_manager.gd create mode 100644 addons/boids/boid_properties/boid_properties.gd rename examples/boid_2d/example_boid.svg => addons/boids/boid_properties/boid_properties.svg (66%) create mode 100644 addons/boids/boid_properties/boid_properties.svg.import create mode 100644 addons/boids/boids.gd create mode 100644 addons/boids/flock/flock.gd rename addons/{boid_2d/boid_2d.svg => boids/flock/flock.svg} (87%) create mode 100644 addons/boids/flock/flock.svg.import create mode 100644 addons/boids/plugin.cfg delete mode 100644 examples/boid_2d/example_boid.tscn delete mode 100644 examples/boid_2d/simple/example.tscn create mode 100644 examples/boids/2d/example_boid.svg rename examples/{boid_2d => boids/2d}/example_boid.svg.import (72%) create mode 100644 examples/boids/2d/example_boid.tscn rename examples/{boid_2d => boids/2d}/follow/example.gd (75%) rename examples/{boid_2d => boids/2d}/follow/example.tscn (60%) rename examples/{boid_2d => boids/2d}/simple/example.gd (56%) create mode 100644 examples/boids/2d/simple/example.tscn diff --git a/addons/boid_2d/boid.gd b/addons/boid_2d/boid.gd deleted file mode 100644 index 1c8d3b5..0000000 --- a/addons/boid_2d/boid.gd +++ /dev/null @@ -1,121 +0,0 @@ -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 deleted file mode 100644 index 0217c0e..0000000 --- a/addons/boid_2d/boid.tscn +++ /dev/null @@ -1,34 +0,0 @@ -[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 deleted file mode 100644 index 1909b35..0000000 --- a/addons/boid_2d/boid_2d.gd +++ /dev/null @@ -1,10 +0,0 @@ -@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/plugin.cfg b/addons/boid_2d/plugin.cfg deleted file mode 100644 index e80610e..0000000 --- a/addons/boid_2d/plugin.cfg +++ /dev/null @@ -1,7 +0,0 @@ -[plugin] - -name="Boid2D" -description="Addon for implementing boids / flocking in Godot." -author="yusdacra" -version="0.1" -script="boid_2d.gd" diff --git a/addons/boids/boid_2d/boid_2d.gd b/addons/boids/boid_2d/boid_2d.gd new file mode 100644 index 0000000..c786ae9 --- /dev/null +++ b/addons/boids/boid_2d/boid_2d.gd @@ -0,0 +1,24 @@ +extends Node2D +class_name Boid2D + +## controls the properties of this boid, deciding how it will behave. +@export var properties: BoidProperties + +# position is .position since this is base Node2D +var velocity := Vector2.ZERO + +# this is assigned by the flock, if this boid is a child of it +var flock: Flock + +## applies some force to this boid. +func apply_force(spatial_force: Vector3) -> void: + var force := Vector2(spatial_force.x, spatial_force.y) + velocity += force + velocity = velocity.limit_length(properties.max_speed) + position += velocity * BoidManager.SIMULATION_RATE + +func _get_boid_position() -> Vector3: + return Vector3(position.x, position.y, 0.0) + +func _get_boid_velocity() -> Vector3: + return Vector3(velocity.x, velocity.y, 0.0) diff --git a/addons/boids/boid_2d/boid_2d.svg b/addons/boids/boid_2d/boid_2d.svg new file mode 100644 index 0000000..78121d3 --- /dev/null +++ b/addons/boids/boid_2d/boid_2d.svg @@ -0,0 +1,68 @@ + + + + + + + + + + + + diff --git a/addons/boid_2d/boid_2d.svg.import b/addons/boids/boid_2d/boid_2d.svg.import similarity index 75% rename from addons/boid_2d/boid_2d.svg.import rename to addons/boids/boid_2d/boid_2d.svg.import index a686f80..4e23251 100644 --- a/addons/boid_2d/boid_2d.svg.import +++ b/addons/boids/boid_2d/boid_2d.svg.import @@ -3,7 +3,7 @@ importer="texture" type="CompressedTexture2D" uid="uid://c32akbxu1rkj8" -path="res://.godot/imported/boid_2d.svg-172f46790795ecd4cef523288fe60368.ctex" +path="res://.godot/imported/boid_2d.svg-4548c85817be0f2e607fa4357493b734.ctex" metadata={ "has_editor_variant": true, "vram_texture": false @@ -11,8 +11,8 @@ metadata={ [deps] -source_file="res://addons/boid_2d/boid_2d.svg" -dest_files=["res://.godot/imported/boid_2d.svg-172f46790795ecd4cef523288fe60368.ctex"] +source_file="res://addons/boids/boid_2d/boid_2d.svg" +dest_files=["res://.godot/imported/boid_2d.svg-4548c85817be0f2e607fa4357493b734.ctex"] [params] diff --git a/addons/boids/boid_3d/boid_3d.gd b/addons/boids/boid_3d/boid_3d.gd new file mode 100644 index 0000000..f1c748c --- /dev/null +++ b/addons/boids/boid_3d/boid_3d.gd @@ -0,0 +1,23 @@ +extends Node3D +class_name Boid3D + +## controls the properties of this boid, deciding how it will behave. +@export var properties: BoidProperties + +# position is .position since this is base Node2D +var velocity := Vector3.ZERO + +# this is assigned by the flock, if this boid is a child of it +var flock: Flock + +## applies some force to this boid. +func apply_force(force: Vector3) -> void: + velocity += force + velocity = velocity.limit_length(properties.max_speed) + position += velocity * BoidManager.SIMULATION_RATE + +func _get_boid_position() -> Vector3: + return position + +func _get_boid_velocity() -> Vector3: + return velocity diff --git a/addons/boids/boid_3d/boid_3d.svg b/addons/boids/boid_3d/boid_3d.svg new file mode 100644 index 0000000..9cc976b --- /dev/null +++ b/addons/boids/boid_3d/boid_3d.svg @@ -0,0 +1,68 @@ + + + + + + + + + + + + diff --git a/addons/boids/boid_3d/boid_3d.svg.import b/addons/boids/boid_3d/boid_3d.svg.import new file mode 100644 index 0000000..9974223 --- /dev/null +++ b/addons/boids/boid_3d/boid_3d.svg.import @@ -0,0 +1,38 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://drjkd138np2bs" +path="res://.godot/imported/boid_3d.svg-88c9926717ca573fea33e015b5e5eabe.ctex" +metadata={ +"has_editor_variant": true, +"vram_texture": false +} + +[deps] + +source_file="res://addons/boids/boid_3d/boid_3d.svg" +dest_files=["res://.godot/imported/boid_3d.svg-88c9926717ca573fea33e015b5e5eabe.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/boids/boid_manager.gd b/addons/boids/boid_manager.gd new file mode 100644 index 0000000..736b629 --- /dev/null +++ b/addons/boids/boid_manager.gd @@ -0,0 +1,147 @@ +extends Node + +# parallelize the work into a new task per n boids +# this seems to help with 1000 boids in a single flock from 400ms to 180ms (before quadtrees) +const PARALLELIZATION_RATE: int = 50 # 50 seems to be the best value? +const EPSILON: float = 0.00001 +# simulate per n physics frame ticks +var SIMULATION_RATE: int = 1 + +var flocks: Dictionary = {} + +func _ready() -> void: + get_tree().node_added.connect(_register_flock) + get_tree().node_removed.connect(_unregister_flock) + + _init_register_flock() + +func _init_register_flock(node: Node = get_tree().root) -> void: + _register_flock(node) + for child: Node in node.get_children(): + _init_register_flock(child) + +func _register_flock(maybe_flock: Node) -> void: + if maybe_flock is not Flock: return + flocks[maybe_flock.get_instance_id()] = maybe_flock + print_verbose("[BoidManager] flock ", maybe_flock, " registered") + +func _unregister_flock(maybe_flock: Node) -> void: + if maybe_flock is not Flock: return + flocks.erase(maybe_flock.get_instance_id()) + print_verbose("[BoidManager] flock ", maybe_flock, " unregistered") + +func _physics_process(delta: float) -> void: + # run the simulation at a given rate + if Engine.get_physics_frames() % SIMULATION_RATE == 0: + _process_boids() + +func _process_boids() -> void: + # organize the work into tasks + var boid_count := 0 + var boids_array_idx := 0 + var args_arrays: Array[Array] = [[]] + var force_arrays: Array[PackedVector3Array] = [PackedVector3Array([])] + for flock: Flock in flocks.values(): + var flock_args := _pack_calc_args_flock(flock) + for boid in flock.boids.values(): + var args := _pack_calc_args_boid(boid, flock_args.duplicate()) + args_arrays[boids_array_idx].append(args) + force_arrays[boids_array_idx].append(Vector3.ZERO) + boid_count += 1 + if boid_count > PARALLELIZATION_RATE: + boid_count = 0 + boids_array_idx += 1 + args_arrays.append([]) + force_arrays.append(PackedVector3Array([])) + + # distribute tasks to threads + # TODO: calculate on main thread if there arent enough boids to warrant doing this + var calc_task := WorkerThreadPool.add_group_task( + _calculate_boid_parallel.bind(args_arrays, force_arrays), + args_arrays.size(), + args_arrays.size(), + true, + ) + WorkerThreadPool.wait_for_group_task_completion(calc_task) + + # apply the forces + for idx in args_arrays.size(): + var args = args_arrays[idx] + var forces = force_arrays[idx] + for iidx in args.size(): + args[iidx].boid.apply_force(forces[iidx]) + +func _pack_calc_args_flock(flock: Flock) -> Dictionary: + var others_pos := PackedVector3Array([]) + var others_vel := PackedVector3Array([]) + for aboid in flock.boids.values(): + others_pos.append(aboid._get_boid_position()) + others_vel.append(aboid._get_boid_velocity()) + var flock_args := { + 'others_pos': others_pos, + 'others_vel': others_vel, + 'goal_seperation': flock.goal_seperation, + 'goal_alignment': flock.goal_alignment, + 'goal_cohesion': flock.goal_cohesion, + } + if flock.target != null: + flock_args['target_position'] = flock.target.global_position + return flock_args + +func _pack_calc_args_boid(boid, args: Dictionary) -> Dictionary: + args['boid'] = boid + args['self_props'] = boid.properties + args['self_vel'] = boid._get_boid_velocity() + args['self_pos'] = boid._get_boid_position() + return args + +func _calculate_boid_parallel(idx: int, read_from: Array[Array], write_to: Array[PackedVector3Array]) -> void: + var args = read_from[idx] + var forces = write_to[idx] + for iidx in args.size(): + var force = _calculate_boid(args[iidx]) + forces[iidx] = force + +func _calculate_boid(args: Dictionary) -> Vector3: + var boid_properties: BoidProperties = args.self_props + var boid_pos: Vector3 = args.self_pos + var boid_vel: Vector3 = args.self_vel + + var steer := Vector3.ZERO + var align := Vector3.ZERO + var cohere := Vector3.ZERO + + var steer_count := 0 + var align_count := 0 + var cohere_count := 0 + + var aboid_idx := 0 + for aboid_pos in args.others_pos: + var dist = boid_pos.distance_to(aboid_pos) + if dist >= EPSILON: + var diff = (boid_pos - aboid_pos).normalized() / dist + if dist < args.goal_seperation: steer += diff; steer_count += 1 + if dist < args.goal_alignment: align += args.others_vel[aboid_idx]; align_count += 1 + if dist < args.goal_cohesion: cohere += aboid_pos; cohere_count += 1 + aboid_idx += 1 + + if steer_count > 0: steer /= steer_count + if align_count > 0: align /= align_count + if cohere_count > 0: cohere /= cohere_count; cohere -= boid_pos + + if align.length() > 0.0: align = (align.normalized() * boid_properties.max_speed - boid_vel).limit_length(boid_properties.max_force) + if steer.length() > 0.0: steer = (steer.normalized() * boid_properties.max_speed - boid_vel).limit_length(boid_properties.max_force) + if cohere.length() > 0.0: cohere = (cohere.normalized() * boid_properties.max_speed - boid_vel).limit_length(boid_properties.max_force) + + var target := Vector3.ZERO + var target_position := args.get('target_position') + if target_position != null: + target = ((target_position - boid_pos) - boid_vel).limit_length(boid_properties.max_force) + + var steer_force := steer * boid_properties.seperation + var align_force := align * boid_properties.alignment + var cohere_force := cohere * boid_properties.cohesion + var target_force := target * boid_properties.targeting + var force := steer_force + align_force + cohere_force + target_force + + return force diff --git a/addons/boids/boid_properties/boid_properties.gd b/addons/boids/boid_properties/boid_properties.gd new file mode 100644 index 0000000..93a451f --- /dev/null +++ b/addons/boids/boid_properties/boid_properties.gd @@ -0,0 +1,17 @@ +extends Resource +class_name BoidProperties + +## controls the maximum speed. +@export var max_speed := 4.0 +## controls the maximum force. +@export var max_force := 1.0 + +@export_group("weights") +## controls how inclined the boid will be to align with the rest of it's flock. +@export var alignment := 1.5 +## controls how inclined the boid will be to cohere together with the rest of it's flock. +@export var cohesion := 1.0 +## controls how inclined the boid will be to separate from the rest of it's flock. +@export var seperation := 1.2 +## controls how inclined the boid will be to go to a target (defined by a flock). +@export var targeting := 0.8 diff --git a/examples/boid_2d/example_boid.svg b/addons/boids/boid_properties/boid_properties.svg similarity index 66% rename from examples/boid_2d/example_boid.svg rename to addons/boids/boid_properties/boid_properties.svg index ffc25c7..d977b04 100644 --- a/examples/boid_2d/example_boid.svg +++ b/addons/boids/boid_properties/boid_properties.svg @@ -2,20 +2,23 @@ + sodipodi:nodetypes="cscc" /> diff --git a/addons/boids/boid_properties/boid_properties.svg.import b/addons/boids/boid_properties/boid_properties.svg.import new file mode 100644 index 0000000..5b61bf9 --- /dev/null +++ b/addons/boids/boid_properties/boid_properties.svg.import @@ -0,0 +1,38 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://bjix5s2wjg1te" +path="res://.godot/imported/boid_properties.svg-65ee4ea68118f8a485d6b21ab00db988.ctex" +metadata={ +"has_editor_variant": true, +"vram_texture": false +} + +[deps] + +source_file="res://addons/boids/boid_properties/boid_properties.svg" +dest_files=["res://.godot/imported/boid_properties.svg-65ee4ea68118f8a485d6b21ab00db988.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/boids/boids.gd b/addons/boids/boids.gd new file mode 100644 index 0000000..14af6ab --- /dev/null +++ b/addons/boids/boids.gd @@ -0,0 +1,18 @@ +@tool +extends EditorPlugin + + +func _enter_tree() -> void: + add_custom_type("BoidProperties", "Resource", preload("boid_properties/boid_properties.gd"), preload("boid_properties/boid_properties.svg")) + add_custom_type("Flock", "Node", preload("flock/flock.gd"), preload("flock/flock.svg")) + add_custom_type("Boid2D", "Node2D", preload("boid_2d/boid_2d.gd"), preload("boid_2d/boid_2d.svg")) + add_custom_type("Boid3D", "Node3D", preload("boid_3d/boid_3d.gd"), preload("boid_3d/boid_3d.svg")) + add_autoload_singleton("BoidManager", "res://addons/boids/boid_manager.gd") + + +func _exit_tree() -> void: + remove_custom_type("Flock") + remove_custom_type("Boid2D") + remove_custom_type("Boid3D") + remove_custom_type("BoidProperties") + remove_autoload_singleton("BoidManager") diff --git a/addons/boids/flock/flock.gd b/addons/boids/flock/flock.gd new file mode 100644 index 0000000..7ac2400 --- /dev/null +++ b/addons/boids/flock/flock.gd @@ -0,0 +1,34 @@ +extends Node +class_name Flock + +@export var goal_seperation: float = 25.0 +@export var goal_alignment: float = 50.0 +@export var goal_cohesion: float = 50.0 + +var boids: Dictionary = {} + +## a node that the flock will try to follow. +## target should be either a Node2D or a Node3D (or any inheritors of these two). +@export var target: Node + +func _ready() -> void: + self.child_entered_tree.connect(_register_boid) + self.child_exiting_tree.connect(_unregister_boid) + + _init_register_boid() + +func _init_register_boid(node: Node = self) -> void: + _register_boid(node) + for child: Node in node.get_children(): + _init_register_boid(child) + +func _register_boid(maybe_boid: Node) -> void: + if maybe_boid is not Boid2D and maybe_boid is not Boid3D: return + maybe_boid.flock = self + boids[maybe_boid.get_instance_id()] = maybe_boid + print_verbose("[", self, "]", " boid ", maybe_boid, " registered") + +func _unregister_boid(maybe_boid: Node) -> void: + if maybe_boid is not Boid2D and maybe_boid is not Boid3D: return + boids.erase(maybe_boid.get_instance_id()) + print_verbose("[", self, "]", " boid ", maybe_boid, " unregistered") diff --git a/addons/boid_2d/boid_2d.svg b/addons/boids/flock/flock.svg similarity index 87% rename from addons/boid_2d/boid_2d.svg rename to addons/boids/flock/flock.svg index ca169d2..5f12e01 100644 --- a/addons/boid_2d/boid_2d.svg +++ b/addons/boids/flock/flock.svg @@ -8,7 +8,7 @@ version="1.1" id="svg1" inkscape:version="1.3.2 (091e20e, 2023-11-25, custom)" - sodipodi:docname="boid_2d.svg" + sodipodi:docname="flock.svg" inkscape:export-filename="..\..\resources\boid_2d.png" inkscape:export-xdpi="768" inkscape:export-ydpi="768" @@ -29,9 +29,9 @@ showgrid="true" inkscape:zoom="24.544781" inkscape:cx="9.513224" - inkscape:cy="4.0538149" - inkscape:window-width="1858" - inkscape:window-height="1057" + inkscape:cy="7.3131636" + inkscape:window-width="1920" + inkscape:window-height="1027" inkscape:window-x="1912" inkscape:window-y="-8" inkscape:window-maximized="1" @@ -60,17 +60,17 @@ inkscape:groupmode="layer" id="layer1"> diff --git a/addons/boids/flock/flock.svg.import b/addons/boids/flock/flock.svg.import new file mode 100644 index 0000000..72000a3 --- /dev/null +++ b/addons/boids/flock/flock.svg.import @@ -0,0 +1,38 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://b8kh6tdumqytn" +path="res://.godot/imported/flock.svg-e863ec0929f57a6863c3b7914e0d4cd3.ctex" +metadata={ +"has_editor_variant": true, +"vram_texture": false +} + +[deps] + +source_file="res://addons/boids/flock/flock.svg" +dest_files=["res://.godot/imported/flock.svg-e863ec0929f57a6863c3b7914e0d4cd3.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/boids/plugin.cfg b/addons/boids/plugin.cfg new file mode 100644 index 0000000..ca55d10 --- /dev/null +++ b/addons/boids/plugin.cfg @@ -0,0 +1,7 @@ +[plugin] + +name="Boids" +description="Addon that implements 2D/3D boids / flocking in Godot." +author="yusdacra" +version="0.1" +script="boids.gd" diff --git a/examples/boid_2d/example_boid.tscn b/examples/boid_2d/example_boid.tscn deleted file mode 100644 index ac55e9d..0000000 --- a/examples/boid_2d/example_boid.tscn +++ /dev/null @@ -1,15 +0,0 @@ -[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/simple/example.tscn b/examples/boid_2d/simple/example.tscn deleted file mode 100644 index 3652ef8..0000000 --- a/examples/boid_2d/simple/example.tscn +++ /dev/null @@ -1,12 +0,0 @@ -[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/examples/boids/2d/example_boid.svg b/examples/boids/2d/example_boid.svg new file mode 100644 index 0000000..d8629cc --- /dev/null +++ b/examples/boids/2d/example_boid.svg @@ -0,0 +1,69 @@ + + + + + + + + + + + + diff --git a/examples/boid_2d/example_boid.svg.import b/examples/boids/2d/example_boid.svg.import similarity index 72% rename from examples/boid_2d/example_boid.svg.import rename to examples/boids/2d/example_boid.svg.import index 845b073..cd13031 100644 --- a/examples/boid_2d/example_boid.svg.import +++ b/examples/boids/2d/example_boid.svg.import @@ -3,15 +3,15 @@ importer="texture" type="CompressedTexture2D" uid="uid://rk5u1wthr0n0" -path="res://.godot/imported/example_boid.svg-ebae3589d3b59182aead052ab0bb5c16.ctex" +path="res://.godot/imported/example_boid.svg-6de905ebc2379a658bac4d710ea0dc0b.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"] +source_file="res://examples/boids/2d/example_boid.svg" +dest_files=["res://.godot/imported/example_boid.svg-6de905ebc2379a658bac4d710ea0dc0b.ctex"] [params] @@ -32,6 +32,6 @@ process/hdr_as_srgb=false process/hdr_clamp_exposure=false process/size_limit=0 detect_3d/compress_to=1 -svg/scale=6.0 +svg/scale=1.2 editor/scale_with_editor_scale=false editor/convert_colors_with_editor_theme=false diff --git a/examples/boids/2d/example_boid.tscn b/examples/boids/2d/example_boid.tscn new file mode 100644 index 0000000..7bd3ecf --- /dev/null +++ b/examples/boids/2d/example_boid.tscn @@ -0,0 +1,34 @@ +[gd_scene load_steps=6 format=3 uid="uid://b2sg3n42rkbx8"] + +[ext_resource type="Script" path="res://addons/boids/boid_2d/boid_2d.gd" id="1_vh1uc"] +[ext_resource type="Texture2D" uid="uid://rk5u1wthr0n0" path="res://examples/boids/2d/example_boid.svg" id="2_jx2vb"] +[ext_resource type="Script" path="res://addons/boids/boid_properties/boid_properties.gd" id="2_up2nk"] + +[sub_resource type="Resource" id="Resource_m74bv"] +script = ExtResource("2_up2nk") +max_speed = 4.0 +max_force = 1.0 +alignment = 1.5 +cohesion = 1.0 +seperation = 1.2 +targeting = 0.8 + +[sub_resource type="GDScript" id="GDScript_ldfpo"] +resource_name = "example_boid_sprite" +script/source = "extends Sprite2D + +@onready var boid: Boid2D = get_parent() + +func _process(delta: float) -> void: + var dir := boid.velocity.normalized() + var target_rot := atan2(dir.y, dir.x) + rotation = move_toward(rotation, target_rot, delta * PI * 2.0 * absf(target_rot - rotation)) +" + +[node name="ExampleBoid" type="Node2D"] +script = ExtResource("1_vh1uc") +properties = SubResource("Resource_m74bv") + +[node name="Sprite2D" type="Sprite2D" parent="."] +texture = ExtResource("2_jx2vb") +script = SubResource("GDScript_ldfpo") diff --git a/examples/boid_2d/follow/example.gd b/examples/boids/2d/follow/example.gd similarity index 75% rename from examples/boid_2d/follow/example.gd rename to examples/boids/2d/follow/example.gd index c6c5cd2..753abb4 100644 --- a/examples/boid_2d/follow/example.gd +++ b/examples/boids/2d/follow/example.gd @@ -1,18 +1,14 @@ 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 boid: Boid2D = 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) + $Flock.add_child(boid) diff --git a/examples/boid_2d/follow/example.tscn b/examples/boids/2d/follow/example.tscn similarity index 60% rename from examples/boid_2d/follow/example.tscn rename to examples/boids/2d/follow/example.tscn index 14a1d93..cefd916 100644 --- a/examples/boid_2d/follow/example.tscn +++ b/examples/boids/2d/follow/example.tscn @@ -1,6 +1,7 @@ -[gd_scene load_steps=3 format=3 uid="uid://ckc0dhvrksfh4"] +[gd_scene load_steps=4 format=3 uid="uid://ckc0dhvrksfh4"] -[ext_resource type="Script" path="res://examples/boid_2d/follow/example.gd" id="1_cb4mx"] +[ext_resource type="Script" path="res://examples/boids/2d/follow/example.gd" id="1_cb4mx"] +[ext_resource type="Script" path="res://addons/boids/flock/flock.gd" id="2_i4bjg"] [sub_resource type="Curve2D" id="Curve2D_ncwi0"] _data = { @@ -17,3 +18,7 @@ curve = SubResource("Curve2D_ncwi0") [node name="PathFollow2D" type="PathFollow2D" parent="Path2D"] position = Vector2(1117, 34) rotation = -2.44507 + +[node name="Flock" type="Node" parent="." node_paths=PackedStringArray("target")] +script = ExtResource("2_i4bjg") +target = NodePath("../Path2D/PathFollow2D") diff --git a/examples/boid_2d/simple/example.gd b/examples/boids/2d/simple/example.gd similarity index 56% rename from examples/boid_2d/simple/example.gd rename to examples/boids/2d/simple/example.gd index fe1c074..cc82b19 100644 --- a/examples/boid_2d/simple/example.gd +++ b/examples/boids/2d/simple/example.gd @@ -1,13 +1,12 @@ extends Node2D - func _ready() -> void: - for i in 100: spawnBoid() + for flock in get_children(): + for i in 100: spawnBoid(flock) - -func spawnBoid() -> void: - var boid: Boid = preload("../example_boid.tscn").instantiate() +func spawnBoid(flock: Flock) -> void: + var boid: Boid2D = 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) + flock.add_child(boid) diff --git a/examples/boids/2d/simple/example.tscn b/examples/boids/2d/simple/example.tscn new file mode 100644 index 0000000..bf244df --- /dev/null +++ b/examples/boids/2d/simple/example.tscn @@ -0,0 +1,22 @@ +[gd_scene load_steps=3 format=3 uid="uid://op0qicvpbjt6"] + +[ext_resource type="Script" path="res://examples/boids/2d/simple/example.gd" id="1_3gcrf"] +[ext_resource type="Script" path="res://addons/boids/flock/flock.gd" id="2_1xeeb"] + +[node name="Example" type="Node2D"] +script = ExtResource("1_3gcrf") + +[node name="Flock" type="Node" parent="."] +script = ExtResource("2_1xeeb") + +[node name="Flock2" type="Node" parent="."] +script = ExtResource("2_1xeeb") + +[node name="Flock3" type="Node" parent="."] +script = ExtResource("2_1xeeb") + +[node name="Flock4" type="Node" parent="."] +script = ExtResource("2_1xeeb") + +[node name="Flock5" type="Node" parent="."] +script = ExtResource("2_1xeeb") diff --git a/project.godot b/project.godot index 8372d9d..3b3c741 100644 --- a/project.godot +++ b/project.godot @@ -11,14 +11,31 @@ config_version=5 [application] config/name="Boid Addon" -run/main_scene="res://examples/boid_2d/simple/example.tscn" +run/main_scene="res://examples/boids/2d/simple/example.tscn" config/features=PackedStringArray("4.3", "GL Compatibility") +[autoload] + +BoidManager="*res://addons/boids/boid_manager.gd" + +[debug] + +settings/stdout/verbose_stdout=true + [editor_plugins] -enabled=PackedStringArray("res://addons/boid_2d/plugin.cfg") +enabled=PackedStringArray("res://addons/boids/plugin.cfg") + +[physics] + +2d/run_on_separate_thread=true +3d/run_on_separate_thread=true [rendering] renderer/rendering_method="gl_compatibility" renderer/rendering_method.mobile="gl_compatibility" + +[threading] + +worker_pool/max_threads=20