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/lib/boids.x86.dll b/addons/boids/lib/boids.x86.dll new file mode 100644 index 0000000..70d0d28 Binary files /dev/null and b/addons/boids/lib/boids.x86.dll differ 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::(); + Engine::singleton().register_singleton(get_singleton_name(), singleton); + } + _ => (), + } + } + + fn on_level_deinit(level: InitLevel) { + if level == InitLevel::Scene { + // Get the `Engine` instance and `StringName` for your singleton. + let mut engine = Engine::singleton(); + let singleton_name = get_singleton_name(); + + // We need to retrieve the pointer to the singleton object, + // as it has to be freed manually - unregistering singleton + // doesn't do it automatically. + let singleton = engine + .get_singleton(singleton_name.clone()) + .expect("cannot retrieve the singleton"); + + // Unregistering singleton and freeing the object itself is needed + // to avoid memory leaks and warnings, especially for hot reloading. + engine.unregister_singleton(singleton_name); + singleton.free(); + } + } +} + +#[derive(GodotClass)] +#[class(init, base=Node)] +pub struct BoidsProcess { + #[export] + process_2d: bool, + #[export] + process_3d: bool, + #[export] + #[init(val = 1)] + process_per_tick: i64, + boids: Option>, + engine: Option>, +} + +impl BoidsProcess { + #[inline(always)] + fn get_boids_singleton(&mut self) -> &mut Gd { + unsafe { self.boids.as_mut().unwrap_unchecked() } + } + + #[inline(always)] + fn get_engine_singleton(&self) -> &Gd { + unsafe { self.engine.as_ref().unwrap_unchecked() } + } +} + +#[godot_api] +impl INode for BoidsProcess { + #[inline(always)] + fn ready(&mut self) { + self.boids = Some(get_singleton()); + self.engine = Some(Engine::singleton()); + } + + #[inline(always)] + fn physics_process(&mut self, _: f64) { + if self.get_engine_singleton().get_physics_frames() % (self.process_per_tick as u64) == 0 { + if self.process_2d { + self.get_boids_singleton().bind_mut().process_boids_2d(); + } + } + } +} + +#[derive(GodotClass)] +#[class(init, base=Object)] +struct Boids { + flocks2d: FxIndexMap>, + boids2d: FxIndexMap>, + base: Base, +} + +impl Boids { + fn register_flock_2d(&mut self, flock_id: i64) { + let flock = godot::global::instance_from_id(flock_id).unwrap().cast(); + self.flocks2d.insert(flock_id, flock); + godot_print!("[Boids] flock {flock_id} registered"); + } + + fn unregister_flock_2d(&mut self, flock_id: i64) { + self.flocks2d.shift_remove(&flock_id); + godot_print!("[Boids] flock {flock_id} unregistered"); + } + + #[inline(always)] + fn register_boid_2d(&mut self, boid_id: i64, boid: Gd) { + self.boids2d.insert(boid_id, boid); + } + + #[inline(always)] + fn unregister_boid_2d(&mut self, boid_id: i64) { + self.boids2d.shift_remove(&boid_id); + } +} + +#[godot_api] +impl Boids { + #[func] + #[inline(always)] + fn process_boids_2d(&mut self) { + process_boids(&mut self.boids2d, &self.flocks2d) + } + + #[func] + #[inline(always)] + fn get_total_boid_count(&self) -> i64 { + self.boids2d.len() as i64 + } +} + +#[inline(always)] +const fn to_glam_vec(godot_vec: Vector3) -> Vec3 { + vec3(godot_vec.x, godot_vec.y, godot_vec.z) +} + +#[inline(always)] +fn process_boids(boids: &mut FxIndexMap>, flocks: &FxIndexMap>) +where + F: Flock + GodotClass, + F: Bounds, + B: Boid + GodotClass, + B: Bounds, +{ + #[cfg(feature = "stats")] + let time = std::time::Instant::now(); + let total_boid_count = boids.len(); + let mut calc_funcs = Vec::with_capacity(total_boid_count); + for (_, flock) in flocks.iter() { + let flock = flock.bind(); + let flock_props = flock.get_flock_properties(); + let target_position = flock.get_target_position(); + let boids = Arc::new(flock.get_boids_posvel()); + for (boid_id, (boid_pos, boid_vel, boid_props)) in flock.get_boids() { + let boid_id = *boid_id; + let flock_props = flock_props.clone(); + let target_position = target_position.clone(); + let boids = boids.clone(); + calc_funcs.push((boid_id, move || { + boid::calculate_boid( + boid_pos, + boid_vel, + boid_props, + flock_props, + boids, + target_position, + ) + })); + } + } + #[cfg(feature = "stats")] + godot_print!( + "[Boids] preparing all calculations took {} ms", + time.elapsed().as_millis() + ); + + #[cfg(feature = "stats")] + let time = std::time::Instant::now(); + let forces: Vec<(i64, Vec3)> = calc_funcs + .into_par_iter() + .fold( + || Vec::<(i64, Vec3)>::with_capacity(total_boid_count / 10), + |mut acc, (boid_id, calc_fn)| { + let force = calc_fn(); + acc.push((boid_id, force)); + acc + }, + ) + .reduce( + || Vec::<(i64, Vec3)>::with_capacity(total_boid_count / 10), + |mut left, mut right| { + left.append(&mut right); + left + }, + ); + #[cfg(feature = "stats")] + godot_print!( + "[Boids] calculating all boids took {} ms", + time.elapsed().as_millis() + ); + + #[cfg(feature = "stats")] + let time = std::time::Instant::now(); + for (boid_id, force) in forces { + let boid = boids.get_mut(&boid_id).unwrap(); + boid.bind_mut().apply_force(force); + } + #[cfg(feature = "stats")] + godot_print!( + "[Boids] applying forces took {} ms", + time.elapsed().as_millis() + ); +}