feat: yay rust rewrite its faster

This commit is contained in:
dusk 2024-08-29 04:51:21 +03:00
parent 54a01c2ecc
commit 07405168e8
Signed by: dusk
SSH Key Fingerprint: SHA256:Abmvag+juovVufZTxyWY8KcVgrznxvBjQpJesv071Aw
39 changed files with 1327 additions and 456 deletions

1
.gitignore vendored
View File

@ -1,3 +1,4 @@
# Godot 4+ specific ignores
.godot/
/android/
rust/target

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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")

View File

@ -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"

View File

@ -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")

View File

@ -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

Binary file not shown.

View File

@ -0,0 +1,4 @@
[gd_scene format=3 uid="uid://c84c62urmhxbm"]
[node name="BoidsProcess" type="BoidsProcess"]
process_2d = true

View File

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@ -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]

View File

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@ -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]

View File

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@ -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]

View File

@ -0,0 +1,78 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="16"
height="16"
viewBox="0 0 16 16.000001"
version="1.1"
id="svg1"
inkscape:version="1.3.2 (091e20e, 2023-11-25, custom)"
sodipodi:docname="flock_2d..svg"
inkscape:export-filename="..\..\resources\boid_2d.png"
inkscape:export-xdpi="768"
inkscape:export-ydpi="768"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="px"
showgrid="true"
inkscape:zoom="24.544781"
inkscape:cx="9.513224"
inkscape:cy="7.3131636"
inkscape:window-width="1920"
inkscape:window-height="1027"
inkscape:window-x="1912"
inkscape:window-y="-8"
inkscape:window-maximized="1"
inkscape:current-layer="layer1">
<inkscape:grid
id="grid1"
units="px"
originx="0"
originy="0"
spacingx="1"
spacingy="1.0000001"
empcolor="#0099e5"
empopacity="0.30196078"
color="#0099e5"
opacity="0.14901961"
empspacing="5"
dotted="false"
gridanglex="30"
gridanglez="30"
visible="true" />
</sodipodi:namedview>
<defs
id="defs1" />
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1">
<path
style="fill:#8da5f3;fill-opacity:1;stroke-width:26.4565;stroke-linejoin:bevel"
d="m 1,0.47939285 c 0,0 7,2.33333335 7,2.97323935 0,0.526761 -7,2.860094 -7,2.860094 2.333333,-2.333333 2.333333,-3.5 0,-5.83333335 z"
id="path1"
sodipodi:nodetypes="cscc" />
<path
style="fill:#8da5f3;fill-opacity:1;stroke-width:26.4565;stroke-linejoin:bevel"
d="m 8.5092732,5.0267612 c 0,0 6.9999998,2.3333333 6.9999998,2.9732393 0,0.526761 -6.9999998,2.8600945 -6.9999998,2.8600945 2.3333328,-2.3333335 2.3333328,-3.5000005 0,-5.8333338 z"
id="path2"
sodipodi:nodetypes="cscc" />
<path
style="fill:#8da5f3;fill-opacity:1;stroke-width:26.4565;stroke-linejoin:bevel"
d="m 2.4897504,9.4619873 c 0,0 7,2.3333327 7,2.9732387 0,0.526761 -7,2.860095 -7,2.860095 2.333333,-2.333334 2.333333,-3.500001 0,-5.8333337 z"
id="path3"
sodipodi:nodetypes="cscc" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@ -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

View File

@ -0,0 +1,78 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="16"
height="16"
viewBox="0 0 16 16.000001"
version="1.1"
id="svg1"
inkscape:version="1.3.2 (091e20e, 2023-11-25, custom)"
sodipodi:docname="flock_3d.svg"
inkscape:export-filename="..\..\resources\boid_2d.png"
inkscape:export-xdpi="768"
inkscape:export-ydpi="768"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="px"
showgrid="true"
inkscape:zoom="24.544781"
inkscape:cx="9.513224"
inkscape:cy="7.3131636"
inkscape:window-width="1920"
inkscape:window-height="1027"
inkscape:window-x="1912"
inkscape:window-y="-8"
inkscape:window-maximized="1"
inkscape:current-layer="layer1">
<inkscape:grid
id="grid1"
units="px"
originx="0"
originy="0"
spacingx="1"
spacingy="1.0000001"
empcolor="#0099e5"
empopacity="0.30196078"
color="#0099e5"
opacity="0.14901961"
empspacing="5"
dotted="false"
gridanglex="30"
gridanglez="30"
visible="true" />
</sodipodi:namedview>
<defs
id="defs1" />
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1">
<path
style="fill:#fc7f7f;fill-opacity:1;stroke-width:26.4565;stroke-linejoin:bevel"
d="m 1,0.47939285 c 0,0 7,2.33333335 7,2.97323935 0,0.526761 -7,2.860094 -7,2.860094 2.333333,-2.333333 2.333333,-3.5 0,-5.83333335 z"
id="path1"
sodipodi:nodetypes="cscc" />
<path
style="fill:#fc7f7f;fill-opacity:1;stroke-width:26.4565;stroke-linejoin:bevel"
d="m 8.5092732,5.0267612 c 0,0 6.9999998,2.3333333 6.9999998,2.9732393 0,0.526761 -6.9999998,2.8600945 -6.9999998,2.8600945 2.3333328,-2.3333335 2.3333328,-3.5000005 0,-5.8333338 z"
id="path2"
sodipodi:nodetypes="cscc" />
<path
style="fill:#fc7f7f;fill-opacity:1;stroke-width:26.4565;stroke-linejoin:bevel"
d="m 2.4897504,9.4619873 c 0,0 7,2.3333327 7,2.9732387 0,0.526761 -7,2.860095 -7,2.860095 2.333333,-2.333334 2.333333,-3.500001 0,-5.8333337 z"
id="path3"
sodipodi:nodetypes="cscc" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@ -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

View File

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@ -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]

View File

@ -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")

View File

@ -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)

View File

@ -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")

64
export_presets.cfg Normal file
View File

@ -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}'"

View File

@ -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

1
rust/.gdignore Normal file
View File

@ -0,0 +1 @@
rust/

340
rust/Cargo.lock generated Normal file
View File

@ -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"

20
rust/Cargo.toml Normal file
View File

@ -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

96
rust/src/boid/boid_2d.rs Normal file
View File

@ -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<BoidProperties>,
props: BoidProperties,
vel: Vec2,
flock_id: i64,
base: Base<Node2D>,
}
#[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::<Flock2D>().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::<Flock2D>();
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()
}
}

0
rust/src/boid/boid_3d.rs Normal file
View File

122
rust/src/boid/mod.rs Normal file
View File

@ -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<Vec<(Vec3, Vec3)>>,
target_position: Option<Vec3>,
) -> 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
}

View File

@ -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,
}

102
rust/src/flock/flock_2d.rs Normal file
View File

@ -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<FlockProperties>,
props: FlockProperties,
#[export]
target: Option<Gd<Node2D>>,
pub boids: FxIndexMap<i64, Gd<Boid2D>>,
base: Base<Node2D>,
}
impl Flock2D {
pub fn register_boid(&mut self, boid_id: i64) {
let boid: Gd<Boid2D> = 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<Vec3> {
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<Item = (&i64, (Vec3, Vec3, BoidProperties))> {
self.boids.iter().map(|(id, boid)| {
let boid = boid.bind();
(
id,
(
boid.get_boid_position(),
boid.get_boid_velocity(),
boid.get_boid_properties().clone(),
),
)
})
}
}

12
rust/src/flock/mod.rs Normal file
View File

@ -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<Vec3>;
fn get_boids(&self) -> impl Iterator<Item = (&i64, (Vec3, Vec3, BoidProperties))>;
fn get_boids_posvel(&self) -> Vec<(Vec3, Vec3)>;
}

View File

@ -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,
}

245
rust/src/lib.rs Normal file
View File

@ -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<K, V> = IndexMap<K, V, FxBuildHasher>;
const SINGLETON_NAME: &str = "Boids";
fn get_singleton_name() -> StringName {
StringName::from(SINGLETON_NAME)
}
fn get_singleton() -> Gd<Boids> {
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::<Object>();
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<Gd<Boids>>,
engine: Option<Gd<Engine>>,
}
impl BoidsProcess {
#[inline(always)]
fn get_boids_singleton(&mut self) -> &mut Gd<Boids> {
unsafe { self.boids.as_mut().unwrap_unchecked() }
}
#[inline(always)]
fn get_engine_singleton(&self) -> &Gd<Engine> {
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<i64, Gd<Flock2D>>,
boids2d: FxIndexMap<i64, Gd<Boid2D>>,
base: Base<Object>,
}
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<Boid2D>) {
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<F, B>(boids: &mut FxIndexMap<i64, Gd<B>>, flocks: &FxIndexMap<i64, Gd<F>>)
where
F: Flock + GodotClass,
F: Bounds<Declarer = DeclUser>,
B: Boid + GodotClass,
B: Bounds<Declarer = DeclUser>,
{
#[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()
);
}