diff --git a/.gitignore b/.gitignore
index 0af181c..2be8296 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,4 @@
# Godot 4+ specific ignores
.godot/
/android/
+rust/target
diff --git a/addons/boids/boid_2d/boid_2d.gd b/addons/boids/boid_2d/boid_2d.gd
deleted file mode 100644
index ca15842..0000000
--- a/addons/boids/boid_2d/boid_2d.gd
+++ /dev/null
@@ -1,26 +0,0 @@
-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
-
-var last_processed_in: int = 0
-
-## 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
-
-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_3d/boid_3d.gd b/addons/boids/boid_3d/boid_3d.gd
deleted file mode 100644
index 29d7a8e..0000000
--- a/addons/boids/boid_3d/boid_3d.gd
+++ /dev/null
@@ -1,25 +0,0 @@
-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
-
-var last_processed_in: int = 0
-
-## applies some force to this boid.
-func apply_force(force: Vector3) -> void:
- velocity += force
- velocity = velocity.limit_length(properties.max_speed)
- position += velocity
-
-func _get_boid_position() -> Vector3:
- return position
-
-func _get_boid_velocity() -> Vector3:
- return velocity
diff --git a/addons/boids/boid_manager.gd b/addons/boids/boid_manager.gd
deleted file mode 100644
index 34ae53c..0000000
--- a/addons/boids/boid_manager.gd
+++ /dev/null
@@ -1,179 +0,0 @@
-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
-
-var flocks: Dictionary = {}
-var total_boid_count: int = 0:
- set(new_count):
- total_boid_count = new_count
- args_array.resize(total_boid_count)
- forces_array.resize(total_boid_count)
-
-# create our arrays for parallel processing
-var args_array: Array[Dictionary] = []
-var forces_array: PackedVector3Array = []
-#var grids: Dictionary = {}
-
-func _ready() -> void:
- get_tree().node_added.connect(_register_flock)
- get_tree().node_removed.connect(_unregister_flock)
-
- _init_register_flock()
-
- args_array.resize(total_boid_count)
- forces_array.resize(total_boid_count)
-
-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
- var flock_id := maybe_flock.get_instance_id()
- flocks[flock_id] = maybe_flock
- #grids[flock_id] = Grid.new()
- print_verbose("[BoidManager] flock ", maybe_flock, " registered")
-
-func _unregister_flock(maybe_flock: Node) -> void:
- if maybe_flock is not Flock: return
- var flock_id := maybe_flock.get_instance_id()
- flocks.erase(flock_id)
- #grids.erase(flock_id)
- print_verbose("[BoidManager] flock ", maybe_flock, " unregistered")
-
-func _physics_process(delta: float) -> void:
- _process_boids()
-
-func _process_boids() -> void:
- var total_parallel_tasks := total_boid_count / PARALLELIZATION_RATE
- if total_boid_count % PARALLELIZATION_RATE > 0: total_parallel_tasks += 1
-
- var boid_count := 0
- # organize the work into tasks
- for flock: Flock in flocks.values():
- var flock_args := _pack_calc_args_flock(flock)
- var boids := flock.boids.values()
- #grids.get(flock.get_instance_id()).build(Vector3.ONE * 1000.0, 30.0, boids)
- for boid in boids:
- var args := _pack_calc_args_boid(flock, boid, flock_args.duplicate())
- args_array[boid_count] = args
- forces_array[boid_count] = Vector3.ZERO
- boid_count += 1
-
- # 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,
- total_parallel_tasks,
- total_parallel_tasks,
- true,
- )
- WorkerThreadPool.wait_for_group_task_completion(calc_task)
-
- # apply the forces
- var idx := 0
- for force in forces_array:
- args_array[idx].boid.apply_force(force)
- idx += 1
-
-func _pack_calc_args_flock(flock: Flock) -> Dictionary:
- var num_of_boids := flock.boids.size()
- var others_pos := PackedVector3Array([]); others_pos.resize(num_of_boids)
- var others_vel := PackedVector3Array([]); others_vel.resize(num_of_boids)
- var idx := 0
- for aboid in flock.boids.values():
- others_pos.set(idx, aboid._get_boid_position())
- others_vel.set(idx, aboid._get_boid_velocity())
- idx += 1
- 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(flock: Flock, boid, args: Dictionary) -> Dictionary:
- #var nearby_boids: Array[Node] = grids.get(flock.get_instance_id()).get_nearby_boids(boid)
- #var others_pos := PackedVector3Array([]); others_pos.resize(nearby_boids.size())
- #var others_vel := PackedVector3Array([]); others_vel.resize(nearby_boids.size())
- #var idx := 0
- #for aboid in nearby_boids:
- #others_pos.set(idx, aboid._get_boid_position())
- #others_vel.set(idx, aboid._get_boid_velocity())
- #idx += 1
- #args['others_pos'] = others_pos
- #args['others_vel'] = others_vel
- 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) -> void:
- var start_from := PARALLELIZATION_RATE * idx
- var end_at := mini(start_from + PARALLELIZATION_RATE, total_boid_count)
- var arg_idx := start_from
- while arg_idx < end_at:
- var force := _calculate_boid(args_array[arg_idx])
- forces_array[arg_idx] = force
- arg_idx += 1
-
-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 goal_seperation: float = args.goal_seperation
- var goal_alignment: float = args.goal_alignment
- var goal_cohesion: float = args.goal_cohesion
- var others_pos: PackedVector3Array = args.others_pos
- var others_vel: PackedVector3Array = args.others_vel
- var aboid_idx := 0
- # iterating over the packed array for pos is faster, we use pos always, vel only in one case
- for aboid_pos in others_pos:
- # faster for when checking, we can just sqrt later for calculating steering
- var dist = boid_pos.distance_squared_to(aboid_pos)
- if dist > EPSILON:
- if dist < goal_seperation:
- var diff = (boid_pos - aboid_pos).normalized() / sqrt(dist)
- steer += diff; steer_count += 1
- if dist < goal_alignment: align += others_vel[aboid_idx]; align_count += 1
- if dist < 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_squared() > 0.0: align = (align.normalized() * boid_properties.max_speed - boid_vel).limit_length(boid_properties.max_force)
- if steer.length_squared() > 0.0: steer = (steer.normalized() * boid_properties.max_speed - boid_vel).limit_length(boid_properties.max_force)
- if cohere.length_squared() > 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
deleted file mode 100644
index 93a451f..0000000
--- a/addons/boids/boid_properties/boid_properties.gd
+++ /dev/null
@@ -1,17 +0,0 @@
-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/addons/boids/boids.gd b/addons/boids/boids.gd
index 14af6ab..60e5e6c 100644
--- a/addons/boids/boids.gd
+++ b/addons/boids/boids.gd
@@ -3,16 +3,8 @@ 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")
+ add_autoload_singleton("BoidsProcess_2D", "res://addons/boids/boids_process_2d.tscn")
func _exit_tree() -> void:
- remove_custom_type("Flock")
- remove_custom_type("Boid2D")
- remove_custom_type("Boid3D")
- remove_custom_type("BoidProperties")
- remove_autoload_singleton("BoidManager")
+ remove_autoload_singleton("BoidsProcess_2D")
diff --git a/addons/boids/boids.gdextension b/addons/boids/boids.gdextension
new file mode 100644
index 0000000..3a77d3a
--- /dev/null
+++ b/addons/boids/boids.gdextension
@@ -0,0 +1,20 @@
+[configuration]
+entry_symbol = "gdext_rust_init"
+compatibility_minimum = 4.2
+reloadable = true
+
+[libraries]
+linux.debug.x86_64 = "res://rust/target/debug/libboids.so"
+linux.release.x86_64 = "res://lib/libboids.x86.so"
+windows.debug.x86_64 = "res://rust/target/debug/boids.dll"
+windows.release.x86_64 = "res://lib/boids.x86.dll"
+macos.release = "res://lib/libboids.x86.dylib"
+macos.release.arm64 = "res://lib/libboids.arm64.dylib"
+
+[icons]
+BoidProperties = "res://addons/boids/resources/boid_properties.svg"
+FlockProperties = "res://addons/boids/resources/flock_properties.svg"
+Flock2D = "res://addons/boids/resources/flock_2d.svg"
+Boid2D = "res://addons/boids/resources/boid_2d.svg"
+Flock3D = "res://addons/boids/resources/flock_3d.svg"
+Boid3D = "res://addons/boids/resources/boid_3d.svg"
diff --git a/addons/boids/flock/flock.gd b/addons/boids/flock/flock.gd
deleted file mode 100644
index 4a5462a..0000000
--- a/addons/boids/flock/flock.gd
+++ /dev/null
@@ -1,36 +0,0 @@
-extends Node
-class_name Flock
-
-@export var goal_seperation: float = 25.0 ** 2
-@export var goal_alignment: float = 50.0 ** 2
-@export var goal_cohesion: float = 50.0 ** 2
-
-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
- BoidManager.total_boid_count += 1
- 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())
- BoidManager.total_boid_count -= 1
- print_verbose("[", self, "]", " boid ", maybe_boid, " unregistered")
diff --git a/addons/boids/grid.gd b/addons/boids/grid.gd
deleted file mode 100644
index a7e8b89..0000000
--- a/addons/boids/grid.gd
+++ /dev/null
@@ -1,122 +0,0 @@
-extends RefCounted
-class_name Grid
-
-var _cells: Dictionary
-var _scale: float
-var size: Vector3
-var scaled_points: Dictionary
-
-func build(unscaled_size: Vector3, scale: float, boids: Array):
- _scale = scale
- size = Vector3(_scale_axis(unscaled_size.x), _scale_axis(unscaled_size.y), _scale_axis(unscaled_size.z))
- _cells.clear()
- scaled_points.clear()
-
- var idx := 0
- for boid in boids:
- var scaled_point := _scale_point(boid._get_boid_position())
- _add_body(boid, scaled_point)
- scaled_points[boid.get_instance_id()] = scaled_point
- idx += 1
-
-
-func _scale_axis(point: float) -> float:
- return floorf(point / _scale)
-
-
-func _scale_point(vector: Vector3) -> Vector3:
- var scaled_point = (vector / _scale).floor()
- scaled_point.x = minf(maxf(scaled_point.x, 0), size.x)
- scaled_point.y = minf(maxf(scaled_point.y, 0), size.y)
- scaled_point.z = minf(maxf(scaled_point.z, 0), size.z)
- return scaled_point
-
-
-func _add_body(body: Node, scaled_point: Vector3) -> void:
- var boids := _cells.get(scaled_point, [])
- boids.append(body)
- _cells[scaled_point] = boids
-
-func _get_cell(x: float, y: float, z: float, write_to: Array[Node]) -> void:
- write_to.append_array(_cells.get(Vector3(x, y, z), []))
-
-func get_nearby_boids(boid: Node) -> Array[Node]:
- var scaled_point: Vector3 = scaled_points[boid.get_instance_id()]
-
- # keep the points in bounds
- var x := minf(maxf(scaled_point.x, 0), size.x)
- var y := minf(maxf(scaled_point.y, 0), size.y)
- var z := minf(maxf(scaled_point.z, 0), size.z)
-
- var results: Array[Node] = []
- var gb := func(x, y, z): _get_cell(x, y, z, results)
- gb.call(x, y, z)
-
- var up := y - 1
- var down := y + 1
- var left := x - 1
- var right := x + 1
- var forwards := z - 1
- var backwards := z + 1
-
- # up
- if up > 0:
- gb.call(x, up, z)
- if left > 0:
- gb.call(left, up, z)
- if right <= size.x:
- gb.call(right, up, z)
- if forwards > 0:
- gb.call(x, up, forwards)
- if left > 0:
- gb.call(left, up, forwards)
- if right <= size.x:
- gb.call(right, up, forwards)
- if backwards <= size.z:
- gb.call(x, up, backwards)
- if left > 0:
- gb.call(left, up, backwards)
- if right <= size.x:
- gb.call(right, up, backwards)
- # down
- if down <= size.y:
- gb.call(x, down, z)
- if left > 0:
- gb.call(left, down, z)
- if right <= size.x:
- gb.call(right, down, z)
- if forwards > 0:
- gb.call(x, down, forwards)
- if left > 0:
- gb.call(left, down, forwards)
- if right <= size.x:
- gb.call(right, down, forwards)
- if backwards <= size.z:
- gb.call(x, down, backwards)
- if left > 0:
- gb.call(left, down, backwards)
- if right <= size.x:
- gb.call(right, down, backwards)
-
- # forwards
- if forwards > 0:
- gb.call(x, y, forwards)
- if left > 0:
- gb.call(left, y, forwards)
- if right <= size.x:
- gb.call(right, y, forwards)
-
- if backwards <= size.z:
- gb.call(x, y, backwards)
- if left > 0:
- gb.call(left, y, backwards)
- if right <= size.x:
- gb.call(right, y, backwards)
-
- # left and right
- if left > 0:
- gb.call(left, y, z)
- if right <= size.x:
- gb.call(right, y, z)
-
- return results
diff --git a/addons/boids/process_boids.tscn b/addons/boids/process_boids.tscn
new file mode 100644
index 0000000..d123314
--- /dev/null
+++ b/addons/boids/process_boids.tscn
@@ -0,0 +1,4 @@
+[gd_scene format=3 uid="uid://c84c62urmhxbm"]
+
+[node name="BoidsProcess" type="BoidsProcess"]
+process_2d = true
diff --git a/addons/boids/boid_2d/boid_2d.svg b/addons/boids/resources/boid_2d.svg
similarity index 100%
rename from addons/boids/boid_2d/boid_2d.svg
rename to addons/boids/resources/boid_2d.svg
diff --git a/addons/boids/boid_2d/boid_2d.svg.import b/addons/boids/resources/boid_2d.svg.import
similarity index 75%
rename from addons/boids/boid_2d/boid_2d.svg.import
rename to addons/boids/resources/boid_2d.svg.import
index 4e23251..658ee0d 100644
--- a/addons/boids/boid_2d/boid_2d.svg.import
+++ b/addons/boids/resources/boid_2d.svg.import
@@ -3,7 +3,7 @@
importer="texture"
type="CompressedTexture2D"
uid="uid://c32akbxu1rkj8"
-path="res://.godot/imported/boid_2d.svg-4548c85817be0f2e607fa4357493b734.ctex"
+path="res://.godot/imported/boid_2d.svg-6eb9c1bb2a917467b7bca481a26670cd.ctex"
metadata={
"has_editor_variant": true,
"vram_texture": false
@@ -11,8 +11,8 @@ metadata={
[deps]
-source_file="res://addons/boids/boid_2d/boid_2d.svg"
-dest_files=["res://.godot/imported/boid_2d.svg-4548c85817be0f2e607fa4357493b734.ctex"]
+source_file="res://addons/boids/resources/boid_2d.svg"
+dest_files=["res://.godot/imported/boid_2d.svg-6eb9c1bb2a917467b7bca481a26670cd.ctex"]
[params]
diff --git a/addons/boids/boid_3d/boid_3d.svg b/addons/boids/resources/boid_3d.svg
similarity index 100%
rename from addons/boids/boid_3d/boid_3d.svg
rename to addons/boids/resources/boid_3d.svg
diff --git a/addons/boids/boid_3d/boid_3d.svg.import b/addons/boids/resources/boid_3d.svg.import
similarity index 75%
rename from addons/boids/boid_3d/boid_3d.svg.import
rename to addons/boids/resources/boid_3d.svg.import
index 9974223..0c7a076 100644
--- a/addons/boids/boid_3d/boid_3d.svg.import
+++ b/addons/boids/resources/boid_3d.svg.import
@@ -3,7 +3,7 @@
importer="texture"
type="CompressedTexture2D"
uid="uid://drjkd138np2bs"
-path="res://.godot/imported/boid_3d.svg-88c9926717ca573fea33e015b5e5eabe.ctex"
+path="res://.godot/imported/boid_3d.svg-7eb8b2fd8a0459a17418f05b04812bab.ctex"
metadata={
"has_editor_variant": true,
"vram_texture": false
@@ -11,8 +11,8 @@ metadata={
[deps]
-source_file="res://addons/boids/boid_3d/boid_3d.svg"
-dest_files=["res://.godot/imported/boid_3d.svg-88c9926717ca573fea33e015b5e5eabe.ctex"]
+source_file="res://addons/boids/resources/boid_3d.svg"
+dest_files=["res://.godot/imported/boid_3d.svg-7eb8b2fd8a0459a17418f05b04812bab.ctex"]
[params]
diff --git a/addons/boids/boid_properties/boid_properties.svg b/addons/boids/resources/boid_properties.svg
similarity index 100%
rename from addons/boids/boid_properties/boid_properties.svg
rename to addons/boids/resources/boid_properties.svg
diff --git a/addons/boids/boid_properties/boid_properties.svg.import b/addons/boids/resources/boid_properties.svg.import
similarity index 73%
rename from addons/boids/boid_properties/boid_properties.svg.import
rename to addons/boids/resources/boid_properties.svg.import
index 5b61bf9..218aa5b 100644
--- a/addons/boids/boid_properties/boid_properties.svg.import
+++ b/addons/boids/resources/boid_properties.svg.import
@@ -3,7 +3,7 @@
importer="texture"
type="CompressedTexture2D"
uid="uid://bjix5s2wjg1te"
-path="res://.godot/imported/boid_properties.svg-65ee4ea68118f8a485d6b21ab00db988.ctex"
+path="res://.godot/imported/boid_properties.svg-a84e56cd5111635c4baa23dc685bed70.ctex"
metadata={
"has_editor_variant": true,
"vram_texture": false
@@ -11,8 +11,8 @@ metadata={
[deps]
-source_file="res://addons/boids/boid_properties/boid_properties.svg"
-dest_files=["res://.godot/imported/boid_properties.svg-65ee4ea68118f8a485d6b21ab00db988.ctex"]
+source_file="res://addons/boids/resources/boid_properties.svg"
+dest_files=["res://.godot/imported/boid_properties.svg-a84e56cd5111635c4baa23dc685bed70.ctex"]
[params]
diff --git a/addons/boids/resources/flock_2d.svg b/addons/boids/resources/flock_2d.svg
new file mode 100644
index 0000000..b77a167
--- /dev/null
+++ b/addons/boids/resources/flock_2d.svg
@@ -0,0 +1,78 @@
+
+
+
+
diff --git a/addons/boids/resources/flock_2d.svg.import b/addons/boids/resources/flock_2d.svg.import
new file mode 100644
index 0000000..d036f7f
--- /dev/null
+++ b/addons/boids/resources/flock_2d.svg.import
@@ -0,0 +1,38 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://vomei5qtdnve"
+path="res://.godot/imported/flock_2d.svg-47deab08c18f15991172d7e7bc5b0fba.ctex"
+metadata={
+"has_editor_variant": true,
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/boids/resources/flock_2d.svg"
+dest_files=["res://.godot/imported/flock_2d.svg-47deab08c18f15991172d7e7bc5b0fba.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/resources/flock_3d.svg b/addons/boids/resources/flock_3d.svg
new file mode 100644
index 0000000..72fb997
--- /dev/null
+++ b/addons/boids/resources/flock_3d.svg
@@ -0,0 +1,78 @@
+
+
+
+
diff --git a/addons/boids/resources/flock_3d.svg.import b/addons/boids/resources/flock_3d.svg.import
new file mode 100644
index 0000000..72eef33
--- /dev/null
+++ b/addons/boids/resources/flock_3d.svg.import
@@ -0,0 +1,38 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://c2uweiyvhkth2"
+path="res://.godot/imported/flock_3d.svg-acc5687a4886765157480151046c93a8.ctex"
+metadata={
+"has_editor_variant": true,
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://addons/boids/resources/flock_3d.svg"
+dest_files=["res://.godot/imported/flock_3d.svg-acc5687a4886765157480151046c93a8.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/flock/flock.svg b/addons/boids/resources/flock_properties.svg
similarity index 100%
rename from addons/boids/flock/flock.svg
rename to addons/boids/resources/flock_properties.svg
diff --git a/addons/boids/flock/flock.svg.import b/addons/boids/resources/flock_properties.svg.import
similarity index 73%
rename from addons/boids/flock/flock.svg.import
rename to addons/boids/resources/flock_properties.svg.import
index 72000a3..98845be 100644
--- a/addons/boids/flock/flock.svg.import
+++ b/addons/boids/resources/flock_properties.svg.import
@@ -3,7 +3,7 @@
importer="texture"
type="CompressedTexture2D"
uid="uid://b8kh6tdumqytn"
-path="res://.godot/imported/flock.svg-e863ec0929f57a6863c3b7914e0d4cd3.ctex"
+path="res://.godot/imported/flock_properties.svg-0c28b950b4cd1e6d443e4480692c1ed5.ctex"
metadata={
"has_editor_variant": true,
"vram_texture": false
@@ -11,8 +11,8 @@ metadata={
[deps]
-source_file="res://addons/boids/flock/flock.svg"
-dest_files=["res://.godot/imported/flock.svg-e863ec0929f57a6863c3b7914e0d4cd3.ctex"]
+source_file="res://addons/boids/resources/flock_properties.svg"
+dest_files=["res://.godot/imported/flock_properties.svg-0c28b950b4cd1e6d443e4480692c1ed5.ctex"]
[params]
diff --git a/examples/boids/2d/example_boid.tscn b/examples/boids/2d/example_boid.tscn
index 7bd3ecf..5d1399d 100644
--- a/examples/boids/2d/example_boid.tscn
+++ b/examples/boids/2d/example_boid.tscn
@@ -1,17 +1,8 @@
-[gd_scene load_steps=6 format=3 uid="uid://b2sg3n42rkbx8"]
+[gd_scene load_steps=4 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="BoidProperties" id="BoidProperties_a6wou"]
[sub_resource type="GDScript" id="GDScript_ldfpo"]
resource_name = "example_boid_sprite"
@@ -20,14 +11,13 @@ script/source = "extends Sprite2D
@onready var boid: Boid2D = get_parent()
func _process(delta: float) -> void:
- var dir := boid.velocity.normalized()
+ var dir := boid.get_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="ExampleBoid" type="Boid2D"]
+properties = SubResource("BoidProperties_a6wou")
[node name="Sprite2D" type="Sprite2D" parent="."]
texture = ExtResource("2_jx2vb")
diff --git a/examples/boids/2d/simple/example.gd b/examples/boids/2d/simple/example.gd
index cc82b19..3f06d49 100644
--- a/examples/boids/2d/simple/example.gd
+++ b/examples/boids/2d/simple/example.gd
@@ -2,9 +2,9 @@ extends Node2D
func _ready() -> void:
for flock in get_children():
- for i in 100: spawnBoid(flock)
+ for i in 1000: spawnBoid(flock)
-func spawnBoid(flock: Flock) -> void:
+func spawnBoid(flock: Flock2D) -> void:
var boid: Boid2D = preload("../example_boid.tscn").instantiate()
var screensize := get_viewport_rect().size
boid.modulate = Color(randf(), randf(), randf(), 1)
diff --git a/examples/boids/2d/simple/example.tscn b/examples/boids/2d/simple/example.tscn
index f9d5684..e5a64b0 100644
--- a/examples/boids/2d/simple/example.tscn
+++ b/examples/boids/2d/simple/example.tscn
@@ -1,10 +1,11 @@
[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"]
+
+[sub_resource type="FlockProperties" id="FlockProperties_cvyp0"]
[node name="Example" type="Node2D"]
script = ExtResource("1_3gcrf")
-[node name="Flock" type="Node" parent="."]
-script = ExtResource("2_1xeeb")
+[node name="Flock" type="Flock2D" parent="."]
+properties = SubResource("FlockProperties_cvyp0")
diff --git a/export_presets.cfg b/export_presets.cfg
new file mode 100644
index 0000000..d38a598
--- /dev/null
+++ b/export_presets.cfg
@@ -0,0 +1,64 @@
+[preset.0]
+
+name="Windows Desktop"
+platform="Windows Desktop"
+runnable=true
+advanced_options=false
+dedicated_server=false
+custom_features=""
+export_filter="all_resources"
+include_filter=""
+exclude_filter=""
+export_path="../boid-addon-out/win.exe"
+encryption_include_filters=""
+encryption_exclude_filters=""
+encrypt_pck=false
+encrypt_directory=false
+script_export_mode=2
+
+[preset.0.options]
+
+custom_template/debug=""
+custom_template/release=""
+debug/export_console_wrapper=1
+binary_format/embed_pck=false
+texture_format/s3tc_bptc=true
+texture_format/etc2_astc=false
+binary_format/architecture="x86_64"
+codesign/enable=false
+codesign/timestamp=true
+codesign/timestamp_server_url=""
+codesign/digest_algorithm=1
+codesign/description=""
+codesign/custom_options=PackedStringArray()
+application/modify_resources=true
+application/icon=""
+application/console_wrapper_icon=""
+application/icon_interpolation=4
+application/file_version=""
+application/product_version=""
+application/company_name=""
+application/product_name=""
+application/file_description=""
+application/copyright=""
+application/trademarks=""
+application/export_angle=0
+application/export_d3d12=0
+application/d3d12_agility_sdk_multiarch=true
+ssh_remote_deploy/enabled=false
+ssh_remote_deploy/host="user@host_ip"
+ssh_remote_deploy/port="22"
+ssh_remote_deploy/extra_args_ssh=""
+ssh_remote_deploy/extra_args_scp=""
+ssh_remote_deploy/run_script="Expand-Archive -LiteralPath '{temp_dir}\\{archive_name}' -DestinationPath '{temp_dir}'
+$action = New-ScheduledTaskAction -Execute '{temp_dir}\\{exe_name}' -Argument '{cmd_args}'
+$trigger = New-ScheduledTaskTrigger -Once -At 00:00
+$settings = New-ScheduledTaskSettingsSet
+$task = New-ScheduledTask -Action $action -Trigger $trigger -Settings $settings
+Register-ScheduledTask godot_remote_debug -InputObject $task -Force:$true
+Start-ScheduledTask -TaskName godot_remote_debug
+while (Get-ScheduledTask -TaskName godot_remote_debug | ? State -eq running) { Start-Sleep -Milliseconds 100 }
+Unregister-ScheduledTask -TaskName godot_remote_debug -Confirm:$false -ErrorAction:SilentlyContinue"
+ssh_remote_deploy/cleanup_script="Stop-ScheduledTask -TaskName godot_remote_debug -ErrorAction:SilentlyContinue
+Unregister-ScheduledTask -TaskName godot_remote_debug -Confirm:$false -ErrorAction:SilentlyContinue
+Remove-Item -Recurse -Force '{temp_dir}'"
diff --git a/project.godot b/project.godot
index c019fcf..3d59818 100644
--- a/project.godot
+++ b/project.godot
@@ -16,11 +16,7 @@ config/features=PackedStringArray("4.3", "GL Compatibility")
[autoload]
-BoidManager="*res://addons/boids/boid_manager.gd"
-
-[debug]
-
-settings/stdout/verbose_stdout=true
+ProcessBoids="*res://addons/boids/process_boids.tscn"
[editor_plugins]
@@ -30,7 +26,3 @@ enabled=PackedStringArray("res://addons/boids/plugin.cfg")
renderer/rendering_method="gl_compatibility"
renderer/rendering_method.mobile="gl_compatibility"
-
-[threading]
-
-worker_pool/max_threads=20
diff --git a/rust/.gdignore b/rust/.gdignore
new file mode 100644
index 0000000..8c750ba
--- /dev/null
+++ b/rust/.gdignore
@@ -0,0 +1 @@
+rust/
diff --git a/rust/Cargo.lock b/rust/Cargo.lock
new file mode 100644
index 0000000..76f0691
--- /dev/null
+++ b/rust/Cargo.lock
@@ -0,0 +1,340 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 3
+
+[[package]]
+name = "aho-corasick"
+version = "1.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "boids"
+version = "0.1.0"
+dependencies = [
+ "glam",
+ "godot",
+ "indexmap",
+ "rayon",
+ "rustc-hash",
+]
+
+[[package]]
+name = "cfg-if"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
+
+[[package]]
+name = "crossbeam-deque"
+version = "0.8.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d"
+dependencies = [
+ "crossbeam-epoch",
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-epoch"
+version = "0.9.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"
+dependencies = [
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-utils"
+version = "0.8.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80"
+
+[[package]]
+name = "either"
+version = "1.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0"
+
+[[package]]
+name = "equivalent"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
+
+[[package]]
+name = "gdextension-api"
+version = "0.2.0"
+source = "git+https://github.com/godot-rust/godot4-prebuilt?branch=releases#6d902e8a6060007f4ab94cd78882247ae2558d96"
+
+[[package]]
+name = "gensym"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "913dce4c5f06c2ea40fc178c06f777ac89fc6b1383e90c254fafb1abe4ba3c82"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+ "uuid",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.2.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "wasi",
+]
+
+[[package]]
+name = "glam"
+version = "0.28.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "779ae4bf7e8421cf91c0b3b64e7e8b40b862fba4d393f59150042de7c4965a94"
+
+[[package]]
+name = "godot"
+version = "0.1.3"
+source = "git+https://github.com/godot-rust/gdext?branch=master#7634fe769d1fcb66209586f0b6c06aac40978253"
+dependencies = [
+ "godot-core",
+ "godot-macros",
+]
+
+[[package]]
+name = "godot-bindings"
+version = "0.1.3"
+source = "git+https://github.com/godot-rust/gdext?branch=master#7634fe769d1fcb66209586f0b6c06aac40978253"
+dependencies = [
+ "gdextension-api",
+]
+
+[[package]]
+name = "godot-cell"
+version = "0.1.3"
+source = "git+https://github.com/godot-rust/gdext?branch=master#7634fe769d1fcb66209586f0b6c06aac40978253"
+
+[[package]]
+name = "godot-codegen"
+version = "0.1.3"
+source = "git+https://github.com/godot-rust/gdext?branch=master#7634fe769d1fcb66209586f0b6c06aac40978253"
+dependencies = [
+ "godot-bindings",
+ "heck",
+ "nanoserde",
+ "proc-macro2",
+ "quote",
+ "regex",
+]
+
+[[package]]
+name = "godot-core"
+version = "0.1.3"
+source = "git+https://github.com/godot-rust/gdext?branch=master#7634fe769d1fcb66209586f0b6c06aac40978253"
+dependencies = [
+ "glam",
+ "godot-bindings",
+ "godot-cell",
+ "godot-codegen",
+ "godot-ffi",
+]
+
+[[package]]
+name = "godot-ffi"
+version = "0.1.3"
+source = "git+https://github.com/godot-rust/gdext?branch=master#7634fe769d1fcb66209586f0b6c06aac40978253"
+dependencies = [
+ "gensym",
+ "godot-bindings",
+ "godot-codegen",
+ "libc",
+ "paste",
+]
+
+[[package]]
+name = "godot-macros"
+version = "0.1.3"
+source = "git+https://github.com/godot-rust/gdext?branch=master#7634fe769d1fcb66209586f0b6c06aac40978253"
+dependencies = [
+ "godot-bindings",
+ "proc-macro2",
+ "quote",
+ "venial",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.14.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
+
+[[package]]
+name = "heck"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
+
+[[package]]
+name = "indexmap"
+version = "2.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93ead53efc7ea8ed3cfb0c79fc8023fbb782a5432b52830b6518941cebe6505c"
+dependencies = [
+ "equivalent",
+ "hashbrown",
+]
+
+[[package]]
+name = "libc"
+version = "0.2.158"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d8adc4bb1803a324070e64a98ae98f38934d91957a99cfb3a43dcbc01bc56439"
+
+[[package]]
+name = "memchr"
+version = "2.7.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
+
+[[package]]
+name = "nanoserde"
+version = "0.1.37"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5de9cf844ab1e25a0353525bd74cb889843a6215fa4a0d156fd446f4857a1b99"
+dependencies = [
+ "nanoserde-derive",
+]
+
+[[package]]
+name = "nanoserde-derive"
+version = "0.1.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e943b2c21337b7e3ec6678500687cdc741b7639ad457f234693352075c082204"
+
+[[package]]
+name = "paste"
+version = "1.0.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.86"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.37"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "rayon"
+version = "1.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa"
+dependencies = [
+ "either",
+ "rayon-core",
+]
+
+[[package]]
+name = "rayon-core"
+version = "1.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2"
+dependencies = [
+ "crossbeam-deque",
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "regex"
+version = "1.10.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4219d74c6b67a3654a9fbebc4b419e22126d13d2f3c4a07ee0cb61ff79a79619"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-automata",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-automata"
+version = "0.4.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-syntax"
+version = "0.8.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b"
+
+[[package]]
+name = "rustc-hash"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "583034fd73374156e66797ed8e5b0d5690409c9226b22d87cb7f19821c05d152"
+
+[[package]]
+name = "syn"
+version = "2.0.76"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "578e081a14e0cefc3279b0472138c513f37b41a08d5a3cca9b6e4e8ceb6cd525"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
+
+[[package]]
+name = "uuid"
+version = "1.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "81dfa00651efa65069b0b6b651f4aaa31ba9e3c3ce0137aaad053604ee7e0314"
+dependencies = [
+ "getrandom",
+]
+
+[[package]]
+name = "venial"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6816bc32f30bf8dd1b3adb04de8406c7bf187d2f923bd9e4c0b99365d012613f"
+dependencies = [
+ "proc-macro2",
+ "quote",
+]
+
+[[package]]
+name = "wasi"
+version = "0.11.0+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
diff --git a/rust/Cargo.toml b/rust/Cargo.toml
new file mode 100644
index 0000000..b85705e
--- /dev/null
+++ b/rust/Cargo.toml
@@ -0,0 +1,20 @@
+[package]
+name = "boids"
+version = "0.1.0"
+edition = "2021"
+
+[lib]
+crate-type = ["cdylib"]
+
+[features]
+stats = []
+
+[dependencies]
+godot = { git = "https://github.com/godot-rust/gdext", branch = "master", features = ["api-4-3", "experimental-threads"] }
+glam = { version = "0.28", features = ["fast-math"] }
+rayon = { version = "1.10" }
+rustc-hash = "2"
+indexmap = "2.4.0"
+
+[profile.dev]
+opt-level = 3
\ No newline at end of file
diff --git a/rust/src/boid/boid_2d.rs b/rust/src/boid/boid_2d.rs
new file mode 100644
index 0000000..0bd75ae
--- /dev/null
+++ b/rust/src/boid/boid_2d.rs
@@ -0,0 +1,96 @@
+use super::*;
+use godot::prelude::*;
+
+use crate::{BoidProperties, Flock2D};
+
+#[derive(GodotClass)]
+#[class(init, base=Node2D)]
+pub struct Boid2D {
+ #[export]
+ properties: Gd,
+ props: BoidProperties,
+ vel: Vec2,
+ flock_id: i64,
+ base: Base,
+}
+
+#[godot_api]
+impl Boid2D {
+ #[func]
+ #[inline(always)]
+ fn get_velocity(&self) -> Vector2 {
+ Vector2::new(self.vel.x, self.vel.y)
+ }
+
+ #[func]
+ #[inline(always)]
+ pub fn get_id(&self) -> i64 {
+ self.base().instance_id().to_i64()
+ }
+
+ #[func]
+ #[inline(always)]
+ pub fn get_flock_id(&self) -> i64 {
+ self.flock_id
+ }
+}
+
+#[godot_api]
+impl INode2D for Boid2D {
+ fn enter_tree(&mut self) {
+ let Some(mut flock) = self
+ .to_gd()
+ .get_parent()
+ .and_then(|gd| gd.try_cast::().ok())
+ else {
+ let boid_id = self.get_id();
+ godot_error!("[Boid2D:{boid_id}] boids parent isn't a Flock2D, or has no parent");
+ return;
+ };
+ let mut flock = flock.bind_mut();
+ flock.register_boid(self.get_id());
+ self.flock_id = flock.get_id();
+ }
+
+ fn ready(&mut self) {
+ self.props = self.properties.bind().clone();
+ }
+
+ fn exit_tree(&mut self) {
+ let mut flock = godot::global::instance_from_id(self.get_flock_id())
+ .unwrap()
+ .cast::();
+ flock.bind_mut().unregister_boid(self.get_id());
+ }
+}
+
+impl Boid for Boid2D {
+ #[inline(always)]
+ fn apply_force(&mut self, force: Vec3) {
+ self.vel += force.xy();
+ let new_vel = self.vel.clamp_length_max(self.props.max_speed);
+ self.vel = new_vel;
+ self.base_mut().translate(Vector2::new(new_vel.x, new_vel.y));
+ }
+
+ #[inline(always)]
+ fn get_boid_position(&self) -> Vec3 {
+ let pos = self.base().get_position();
+ vec3(pos.x, pos.y, 0.0)
+ }
+
+ #[inline(always)]
+ fn get_boid_velocity(&self) -> Vec3 {
+ vec3(self.vel.x, self.vel.y, 0.0)
+ }
+
+ #[inline(always)]
+ fn get_boid_properties(&self) -> &BoidProperties {
+ &self.props
+ }
+
+ #[inline(always)]
+ fn get_flock_id(&self) -> i64 {
+ self.get_flock_id()
+ }
+}
diff --git a/rust/src/boid/boid_3d.rs b/rust/src/boid/boid_3d.rs
new file mode 100644
index 0000000..e69de29
diff --git a/rust/src/boid/mod.rs b/rust/src/boid/mod.rs
new file mode 100644
index 0000000..3daf4c4
--- /dev/null
+++ b/rust/src/boid/mod.rs
@@ -0,0 +1,122 @@
+use std::ops::Sub;
+use std::sync::Arc;
+
+use glam::*;
+use rayon::prelude::*;
+
+use crate::{BoidProperties, FlockProperties};
+
+pub mod boid_2d;
+pub mod boid_3d;
+
+pub trait Boid {
+ fn apply_force(&mut self, force: Vec3);
+ fn get_boid_position(&self) -> Vec3;
+ fn get_boid_velocity(&self) -> Vec3;
+ fn get_boid_properties(&self) -> &BoidProperties;
+
+ fn get_flock_id(&self) -> i64;
+}
+
+struct CalcArgs {
+ steer: Vec3,
+ align: Vec3,
+ cohere: Vec3,
+
+ steer_count: i32,
+ align_count: i32,
+ cohere_count: i32,
+}
+
+impl CalcArgs {
+ const fn identity() -> Self {
+ Self {
+ steer: Vec3::ZERO,
+ align: Vec3::ZERO,
+ cohere: Vec3::ZERO,
+ steer_count: 0,
+ align_count: 0,
+ cohere_count: 0,
+ }
+ }
+}
+
+pub fn calculate_boid(
+ boid_pos: Vec3,
+ boid_vel: Vec3,
+ boid_props: BoidProperties,
+ flock_props: FlockProperties,
+ boids: Arc>,
+ target_position: Option,
+) -> Vec3 {
+ //godot::godot_print!("[Boids] executing from thread {:?}", rayon::current_thread_index());
+
+ let mut calced = boids
+ .par_iter()
+ .fold(CalcArgs::identity, |mut acc, (aboid_pos, aboid_vel)| {
+ let dist = boid_pos.distance_squared(*aboid_pos);
+ if dist > f32::EPSILON {
+ if dist < flock_props.goal_seperation {
+ let diff = (boid_pos.sub(*aboid_pos)).normalize() / f32::sqrt(dist);
+ acc.steer += diff;
+ acc.steer_count += 1;
+ }
+ if dist < flock_props.goal_alignment {
+ acc.align += *aboid_vel;
+ acc.align_count += 1;
+ }
+ if dist < flock_props.goal_cohesion {
+ acc.cohere += *aboid_pos;
+ acc.cohere_count += 1;
+ }
+ }
+ acc
+ })
+ .reduce(CalcArgs::identity, |mut left, right| {
+ left.steer += right.steer;
+ left.align += right.align;
+ left.cohere += right.cohere;
+ left.steer_count += right.steer_count;
+ left.align_count += right.align_count;
+ left.cohere_count += right.cohere_count;
+ left
+ });
+
+ if calced.steer_count > 0 {
+ calced.steer /= calced.steer_count as f32;
+ }
+ if calced.align_count > 0 {
+ calced.align /= calced.align_count as f32;
+ }
+ if calced.cohere_count > 0 {
+ calced.cohere /= calced.cohere_count as f32;
+ calced.cohere -= boid_pos;
+ }
+
+ let max_speed = boid_props.max_speed;
+ let max_force = boid_props.max_force;
+ if calced.align.length_squared() > 0.0 {
+ calced.align =
+ (calced.align.normalize() * max_speed - boid_vel).clamp_length_max(max_force);
+ }
+ if calced.steer.length_squared() > 0.0 {
+ calced.steer =
+ (calced.steer.normalize() * max_speed - boid_vel).clamp_length_max(max_force);
+ }
+ if calced.cohere.length_squared() > 0.0 {
+ calced.cohere =
+ (calced.cohere.normalize() * max_speed - boid_vel).clamp_length_max(max_force);
+ }
+
+ let target = target_position.map_or(Vec3::ZERO, |target_position| {
+ ((target_position - boid_pos) - boid_vel).clamp_length_max(max_force)
+ });
+
+ let steer_force = calced.steer * boid_props.seperation;
+ let align_force = calced.align * boid_props.alignment;
+ let cohere_force = calced.cohere * boid_props.cohesion;
+ let target_force = target * boid_props.targeting;
+ let force = steer_force + align_force + cohere_force + target_force;
+
+ force
+}
diff --git a/rust/src/boid_properties.rs b/rust/src/boid_properties.rs
new file mode 100644
index 0000000..e91728f
--- /dev/null
+++ b/rust/src/boid_properties.rs
@@ -0,0 +1,24 @@
+use godot::prelude::*;
+
+#[derive(Default, Clone, Debug, GodotClass)]
+#[class(tool, init, base=Resource)]
+pub struct BoidProperties {
+ #[export]
+ #[init(val = 4.0)]
+ pub max_speed: f32,
+ #[export]
+ #[init(val = 1.0)]
+ pub max_force: f32,
+ #[export]
+ #[init(val = 1.5)]
+ pub alignment: f32,
+ #[export]
+ #[init(val = 1.0)]
+ pub cohesion: f32,
+ #[export]
+ #[init(val = 1.2)]
+ pub seperation: f32,
+ #[export]
+ #[init(val = 0.8)]
+ pub targeting: f32,
+}
\ No newline at end of file
diff --git a/rust/src/flock/flock_2d.rs b/rust/src/flock/flock_2d.rs
new file mode 100644
index 0000000..2e5a002
--- /dev/null
+++ b/rust/src/flock/flock_2d.rs
@@ -0,0 +1,102 @@
+use glam::*;
+use godot::prelude::*;
+
+use crate::{get_singleton, Boid, Boid2D, BoidProperties, FlockProperties, FxIndexMap};
+
+use super::Flock;
+
+#[derive(GodotClass)]
+#[class(init, base=Node2D)]
+pub struct Flock2D {
+ #[export]
+ properties: Gd,
+ props: FlockProperties,
+ #[export]
+ target: Option>,
+ pub boids: FxIndexMap>,
+ base: Base,
+}
+
+impl Flock2D {
+ pub fn register_boid(&mut self, boid_id: i64) {
+ let boid: Gd = godot::global::instance_from_id(boid_id).unwrap().cast();
+ self.boids.insert(boid_id, boid.clone());
+ get_singleton().bind_mut().register_boid_2d(boid_id, boid);
+ let flock_id = self.get_id();
+ godot_print!("[Flock2D:{flock_id}] boid {boid_id} registered");
+ }
+
+ pub fn unregister_boid(&mut self, boid_id: i64) {
+ self.boids.shift_remove(&boid_id);
+ get_singleton().bind_mut().unregister_boid_2d(boid_id);
+ let flock_id = self.get_id();
+ godot_print!("[Flock2D:{flock_id}] boid {boid_id} unregistered");
+ }
+}
+
+#[godot_api]
+impl INode2D for Flock2D {
+ fn enter_tree(&mut self) {
+ get_singleton().bind_mut().register_flock_2d(self.get_id())
+ }
+
+ fn ready(&mut self) {
+ self.props = self.properties.bind().clone();
+ }
+
+ fn exit_tree(&mut self) {
+ get_singleton()
+ .bind_mut()
+ .unregister_flock_2d(self.get_id())
+ }
+}
+
+#[godot_api]
+impl Flock2D {
+ #[func]
+ #[inline(always)]
+ pub fn get_id(&self) -> i64 {
+ self.base().instance_id().to_i64()
+ }
+}
+
+impl Flock for Flock2D {
+ #[inline(always)]
+ fn get_flock_properties(&self) -> &FlockProperties {
+ &self.props
+ }
+
+ #[inline(always)]
+ fn get_target_position(&self) -> Option {
+ self.target.as_ref().map(|t| {
+ let pos = t.get_position();
+ vec3(pos.x, pos.y, 0.0)
+ })
+ }
+
+ #[inline(always)]
+ fn get_boids_posvel(&self) -> Vec<(Vec3, Vec3)> {
+ let boid_count = self.boids.len();
+ let mut result = Vec::with_capacity(boid_count);
+ result.extend(self.boids.values().map(|b| {
+ let b = b.bind();
+ (b.get_boid_position(), b.get_boid_velocity())
+ }));
+ result
+ }
+
+ #[inline(always)]
+ fn get_boids(&self) -> impl Iterator- {
+ self.boids.iter().map(|(id, boid)| {
+ let boid = boid.bind();
+ (
+ id,
+ (
+ boid.get_boid_position(),
+ boid.get_boid_velocity(),
+ boid.get_boid_properties().clone(),
+ ),
+ )
+ })
+ }
+}
diff --git a/rust/src/flock/mod.rs b/rust/src/flock/mod.rs
new file mode 100644
index 0000000..e7e9a1f
--- /dev/null
+++ b/rust/src/flock/mod.rs
@@ -0,0 +1,12 @@
+use crate::{BoidProperties, FlockProperties};
+
+use glam::*;
+
+pub mod flock_2d;
+
+pub trait Flock {
+ fn get_flock_properties(&self) -> &FlockProperties;
+ fn get_target_position(&self) -> Option;
+ fn get_boids(&self) -> impl Iterator
- ;
+ fn get_boids_posvel(&self) -> Vec<(Vec3, Vec3)>;
+}
diff --git a/rust/src/flock_properties.rs b/rust/src/flock_properties.rs
new file mode 100644
index 0000000..001d084
--- /dev/null
+++ b/rust/src/flock_properties.rs
@@ -0,0 +1,18 @@
+use godot::prelude::*;
+
+#[derive(Default, Clone, Debug, GodotClass)]
+#[class(tool, init, base=Resource)]
+pub struct FlockProperties {
+ #[export]
+ #[init(val = 625.0)]
+ /// squared
+ pub goal_seperation: f32,
+ #[export]
+ #[init(val = 2500.0)]
+ /// squared
+ pub goal_alignment: f32,
+ #[export]
+ #[init(val = 2500.0)]
+ /// squared
+ pub goal_cohesion: f32,
+}
\ No newline at end of file
diff --git a/rust/src/lib.rs b/rust/src/lib.rs
new file mode 100644
index 0000000..2edc8d1
--- /dev/null
+++ b/rust/src/lib.rs
@@ -0,0 +1,245 @@
+use std::sync::Arc;
+
+use glam::*;
+use godot::{
+ classes::Engine,
+ obj::{bounds::DeclUser, Bounds},
+ prelude::*,
+};
+use indexmap::IndexMap;
+use rayon::prelude::*;
+
+mod boid;
+mod boid_properties;
+mod flock;
+mod flock_properties;
+
+pub use boid::{boid_2d::*, boid_3d::*, Boid};
+pub use boid_properties::BoidProperties;
+pub use flock::{flock_2d::*, Flock};
+pub use flock_properties::FlockProperties;
+
+use rustc_hash::FxBuildHasher;
+
+type FxIndexMap = IndexMap;
+
+const SINGLETON_NAME: &str = "Boids";
+
+fn get_singleton_name() -> StringName {
+ StringName::from(SINGLETON_NAME)
+}
+
+fn get_singleton() -> Gd {
+ Engine::singleton()
+ .get_singleton(get_singleton_name())
+ .unwrap()
+ .cast()
+}
+
+struct BoidsExtension;
+
+#[gdextension]
+unsafe impl ExtensionLibrary for BoidsExtension {
+ fn on_level_init(level: InitLevel) {
+ match level {
+ InitLevel::Scene => {
+ let singleton = Boids::new_alloc().upcast::