feat: rewrite in a more performant way, support 3d

This commit is contained in:
dusk 2024-08-26 11:55:38 +03:00
parent 1013d758c9
commit a26e1fa993
Signed by: dusk
SSH Key Fingerprint: SHA256:Abmvag+juovVufZTxyWY8KcVgrznxvBjQpJesv071Aw
29 changed files with 709 additions and 243 deletions

View File

@ -1,121 +0,0 @@
extends Area2D
class_name Boid
## sets the `RayCast2D` used to detect walls.
@export var wallcast: RayCast2D
## sets the `Area2D` used for vision (seeing other boids).
@export var vision: Area2D
## sets the rotate timer, allowing boids to perform random rotations based on the timer's timeout signal.
@export var rotate_timer: Timer
@export_group("properties")
## controls the target (max) speed.
@export var target_speed := 6.0
## controls how much other boids affect this boid.
## higher values will make them more dispersed.
@export var steer_away_factor := 40
## controls whether or not to run collisions before running boid calculations.
## enabling this can help reduce boids escaping colliders, especially if they are following something.
@export var collide_first := false
@export_group("follow")
## controls which node to try and follow, if any
@export var follow_point: Node2D
## controls the radius at which the boid will target, instead of the target directly
@export var follow_radius := 100.0
var last_follow_pos: Vector2 = Vector2.ZERO
var follow_target: Vector2
var speed := target_speed
var vel := Vector2.ZERO
var boidsSeen: Dictionary = {}
func _ready() -> void:
assert(wallcast, "boid invalid: wallcast (RayCast3D) not assigned")
assert(vision, "boid invalid: vision (Area2D) not assigned")
if rotate_timer:
rotate_timer.timeout.connect(_on_rotate_timer_timeout)
func _physics_process(delta: float) -> void:
if collide_first:
_process_collision()
_process_boids()
else:
_process_boids()
_process_collision()
# move boid
var vel_dir := vel.normalized()
# fix if a boid stops by getting seperated and its vel being cancelled at the same time
if vel_dir.is_zero_approx(): vel_dir = Vector2.RIGHT
vel = vel_dir * speed
global_position += vel
# rotate boid
global_rotation = atan2(vel_dir.y, vel_dir.x)
func _process_boids() -> void:
var numOfBoids := boidsSeen.size()
var avgVel := Vector2.ZERO
var avgPos := Vector2.ZERO
var steerAway := Vector2.ZERO
if numOfBoids > 0:
for boid: Boid in boidsSeen.values():
avgVel += boid.vel; avgPos += boid.global_position
var dist := boid.global_position - global_position
steerAway -= dist * (steer_away_factor / dist.length())
# apply follow point vel
if follow_point:
var dist_to_follow := global_position.distance_to(follow_point.global_position)
if global_position.distance_to(follow_target) < 10.0 or dist_to_follow > follow_radius:
_calc_follow_target()
# slow down speed when nearing target
speed = maxf(0.0, lerpf(target_speed, 0.0, follow_radius / dist_to_follow))
var target_vel := (follow_point.global_position - last_follow_pos) * Engine.physics_ticks_per_second
avgVel += target_vel
avgPos += follow_target
var dist := follow_target - global_position
steerAway -= dist * ((steer_away_factor + follow_radius) / dist.length())
numOfBoids += 1
last_follow_pos = follow_point.global_position
if numOfBoids > 0:
avgVel /= numOfBoids
vel += (avgVel - vel) / 2
avgPos /= numOfBoids
vel += avgPos - global_position
steerAway /= numOfBoids
vel += steerAway
func _calc_follow_target() -> void:
var follow_vec := follow_point.global_position - global_position
var target_length := follow_vec.length() + follow_radius
follow_target = global_position + follow_vec.normalized() * target_length
func _process_collision() -> void:
wallcast.force_raycast_update()
if not wallcast.is_colliding(): return
var col_normal: Vector2 = wallcast.get_collision_normal()
vel = vel.bounce(col_normal)
func _on_vision_area_entered(area: Area2D) -> void:
if area == self: return
boidsSeen[area.get_instance_id()] = area
func _on_vision_area_exited(area: Area2D) -> void:
boidsSeen.erase(area.get_instance_id())
func _on_rotate_timer_timeout() -> void:
vel -= Vector2(randf(), randf()) * speed
rotate_timer.start()

View File

@ -1,34 +0,0 @@
[gd_scene load_steps=4 format=3 uid="uid://bq7s2yf0fohes"]
[ext_resource type="Script" path="res://addons/boid_2d/boid.gd" id="1_xwhwb"]
[sub_resource type="RectangleShape2D" id="RectangleShape2D_ackok"]
size = Vector2(32, 16)
[sub_resource type="RectangleShape2D" id="RectangleShape2D_ipkm3"]
size = Vector2(60, 60)
[node name="Boid" type="Area2D" node_paths=PackedStringArray("wallcast", "vision")]
collision_mask = 0
monitoring = false
script = ExtResource("1_xwhwb")
wallcast = NodePath("WallCast")
vision = NodePath("Vision")
[node name="CollisionShape2D" type="CollisionShape2D" parent="."]
shape = SubResource("RectangleShape2D_ackok")
[node name="WallCast" type="RayCast2D" parent="."]
enabled = false
target_position = Vector2(50, 0)
[node name="Vision" type="Area2D" parent="."]
collision_layer = 0
monitorable = false
[node name="CollisionShape2D" type="CollisionShape2D" parent="Vision"]
position = Vector2(42, 0)
shape = SubResource("RectangleShape2D_ipkm3")
[connection signal="area_entered" from="Vision" to="." method="_on_vision_area_entered"]
[connection signal="area_exited" from="Vision" to="." method="_on_vision_area_exited"]

View File

@ -1,10 +0,0 @@
@tool
extends EditorPlugin
func _enter_tree() -> void:
add_custom_type("Boid2D", "Area2D", preload("boid.gd"), preload("boid_2d.svg"))
func _exit_tree() -> void:
remove_custom_type("Boid2D")

View File

@ -1,7 +0,0 @@
[plugin]
name="Boid2D"
description="Addon for implementing boids / flocking in Godot."
author="yusdacra"
version="0.1"
script="boid_2d.gd"

View File

@ -0,0 +1,24 @@
extends Node2D
class_name Boid2D
## controls the properties of this boid, deciding how it will behave.
@export var properties: BoidProperties
# position is .position since this is base Node2D
var velocity := Vector2.ZERO
# this is assigned by the flock, if this boid is a child of it
var flock: Flock
## applies some force to this boid.
func apply_force(spatial_force: Vector3) -> void:
var force := Vector2(spatial_force.x, spatial_force.y)
velocity += force
velocity = velocity.limit_length(properties.max_speed)
position += velocity * BoidManager.SIMULATION_RATE
func _get_boid_position() -> Vector3:
return Vector3(position.x, position.y, 0.0)
func _get_boid_velocity() -> Vector3:
return Vector3(velocity.x, velocity.y, 0.0)

View File

@ -0,0 +1,68 @@
<?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="boid_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="34.711562"
inkscape:cx="6.4387768"
inkscape:cy="7.7639836"
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:52.913;stroke-linejoin:bevel"
d="m 1,2.3333337 c 0,0 14,4.6666666 14,5.9464786 C 15,9.3333343 1,14.000001 1,14.000001 5.666666,9.3333343 5.666666,7.0000003 1,2.3333337 Z"
id="path1"
sodipodi:nodetypes="cscc" />
</g>
</svg>

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-172f46790795ecd4cef523288fe60368.ctex"
path="res://.godot/imported/boid_2d.svg-4548c85817be0f2e607fa4357493b734.ctex"
metadata={
"has_editor_variant": true,
"vram_texture": false
@ -11,8 +11,8 @@ metadata={
[deps]
source_file="res://addons/boid_2d/boid_2d.svg"
dest_files=["res://.godot/imported/boid_2d.svg-172f46790795ecd4cef523288fe60368.ctex"]
source_file="res://addons/boids/boid_2d/boid_2d.svg"
dest_files=["res://.godot/imported/boid_2d.svg-4548c85817be0f2e607fa4357493b734.ctex"]
[params]

View File

@ -0,0 +1,23 @@
extends Node3D
class_name Boid3D
## controls the properties of this boid, deciding how it will behave.
@export var properties: BoidProperties
# position is .position since this is base Node2D
var velocity := Vector3.ZERO
# this is assigned by the flock, if this boid is a child of it
var flock: Flock
## applies some force to this boid.
func apply_force(force: Vector3) -> void:
velocity += force
velocity = velocity.limit_length(properties.max_speed)
position += velocity * BoidManager.SIMULATION_RATE
func _get_boid_position() -> Vector3:
return position
func _get_boid_velocity() -> Vector3:
return velocity

View File

@ -0,0 +1,68 @@
<?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="boid_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="34.711562"
inkscape:cx="6.4387768"
inkscape:cy="7.7639837"
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:52.913;stroke-linejoin:bevel"
d="m 1,2.3333337 c 0,0 14,4.6666666 14,5.9464786 C 15,9.3333343 1,14.000001 1,14.000001 5.666666,9.3333343 5.666666,7.0000003 1,2.3333337 Z"
id="path1"
sodipodi:nodetypes="cscc" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@ -0,0 +1,38 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://drjkd138np2bs"
path="res://.godot/imported/boid_3d.svg-88c9926717ca573fea33e015b5e5eabe.ctex"
metadata={
"has_editor_variant": true,
"vram_texture": false
}
[deps]
source_file="res://addons/boids/boid_3d/boid_3d.svg"
dest_files=["res://.godot/imported/boid_3d.svg-88c9926717ca573fea33e015b5e5eabe.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
svg/scale=1.0
editor/scale_with_editor_scale=true
editor/convert_colors_with_editor_theme=true

View File

@ -0,0 +1,147 @@
extends Node
# parallelize the work into a new task per n boids
# this seems to help with 1000 boids in a single flock from 400ms to 180ms (before quadtrees)
const PARALLELIZATION_RATE: int = 50 # 50 seems to be the best value?
const EPSILON: float = 0.00001
# simulate per n physics frame ticks
var SIMULATION_RATE: int = 1
var flocks: Dictionary = {}
func _ready() -> void:
get_tree().node_added.connect(_register_flock)
get_tree().node_removed.connect(_unregister_flock)
_init_register_flock()
func _init_register_flock(node: Node = get_tree().root) -> void:
_register_flock(node)
for child: Node in node.get_children():
_init_register_flock(child)
func _register_flock(maybe_flock: Node) -> void:
if maybe_flock is not Flock: return
flocks[maybe_flock.get_instance_id()] = maybe_flock
print_verbose("[BoidManager] flock ", maybe_flock, " registered")
func _unregister_flock(maybe_flock: Node) -> void:
if maybe_flock is not Flock: return
flocks.erase(maybe_flock.get_instance_id())
print_verbose("[BoidManager] flock ", maybe_flock, " unregistered")
func _physics_process(delta: float) -> void:
# run the simulation at a given rate
if Engine.get_physics_frames() % SIMULATION_RATE == 0:
_process_boids()
func _process_boids() -> void:
# organize the work into tasks
var boid_count := 0
var boids_array_idx := 0
var args_arrays: Array[Array] = [[]]
var force_arrays: Array[PackedVector3Array] = [PackedVector3Array([])]
for flock: Flock in flocks.values():
var flock_args := _pack_calc_args_flock(flock)
for boid in flock.boids.values():
var args := _pack_calc_args_boid(boid, flock_args.duplicate())
args_arrays[boids_array_idx].append(args)
force_arrays[boids_array_idx].append(Vector3.ZERO)
boid_count += 1
if boid_count > PARALLELIZATION_RATE:
boid_count = 0
boids_array_idx += 1
args_arrays.append([])
force_arrays.append(PackedVector3Array([]))
# distribute tasks to threads
# TODO: calculate on main thread if there arent enough boids to warrant doing this
var calc_task := WorkerThreadPool.add_group_task(
_calculate_boid_parallel.bind(args_arrays, force_arrays),
args_arrays.size(),
args_arrays.size(),
true,
)
WorkerThreadPool.wait_for_group_task_completion(calc_task)
# apply the forces
for idx in args_arrays.size():
var args = args_arrays[idx]
var forces = force_arrays[idx]
for iidx in args.size():
args[iidx].boid.apply_force(forces[iidx])
func _pack_calc_args_flock(flock: Flock) -> Dictionary:
var others_pos := PackedVector3Array([])
var others_vel := PackedVector3Array([])
for aboid in flock.boids.values():
others_pos.append(aboid._get_boid_position())
others_vel.append(aboid._get_boid_velocity())
var flock_args := {
'others_pos': others_pos,
'others_vel': others_vel,
'goal_seperation': flock.goal_seperation,
'goal_alignment': flock.goal_alignment,
'goal_cohesion': flock.goal_cohesion,
}
if flock.target != null:
flock_args['target_position'] = flock.target.global_position
return flock_args
func _pack_calc_args_boid(boid, args: Dictionary) -> Dictionary:
args['boid'] = boid
args['self_props'] = boid.properties
args['self_vel'] = boid._get_boid_velocity()
args['self_pos'] = boid._get_boid_position()
return args
func _calculate_boid_parallel(idx: int, read_from: Array[Array], write_to: Array[PackedVector3Array]) -> void:
var args = read_from[idx]
var forces = write_to[idx]
for iidx in args.size():
var force = _calculate_boid(args[iidx])
forces[iidx] = force
func _calculate_boid(args: Dictionary) -> Vector3:
var boid_properties: BoidProperties = args.self_props
var boid_pos: Vector3 = args.self_pos
var boid_vel: Vector3 = args.self_vel
var steer := Vector3.ZERO
var align := Vector3.ZERO
var cohere := Vector3.ZERO
var steer_count := 0
var align_count := 0
var cohere_count := 0
var aboid_idx := 0
for aboid_pos in args.others_pos:
var dist = boid_pos.distance_to(aboid_pos)
if dist >= EPSILON:
var diff = (boid_pos - aboid_pos).normalized() / dist
if dist < args.goal_seperation: steer += diff; steer_count += 1
if dist < args.goal_alignment: align += args.others_vel[aboid_idx]; align_count += 1
if dist < args.goal_cohesion: cohere += aboid_pos; cohere_count += 1
aboid_idx += 1
if steer_count > 0: steer /= steer_count
if align_count > 0: align /= align_count
if cohere_count > 0: cohere /= cohere_count; cohere -= boid_pos
if align.length() > 0.0: align = (align.normalized() * boid_properties.max_speed - boid_vel).limit_length(boid_properties.max_force)
if steer.length() > 0.0: steer = (steer.normalized() * boid_properties.max_speed - boid_vel).limit_length(boid_properties.max_force)
if cohere.length() > 0.0: cohere = (cohere.normalized() * boid_properties.max_speed - boid_vel).limit_length(boid_properties.max_force)
var target := Vector3.ZERO
var target_position := args.get('target_position')
if target_position != null:
target = ((target_position - boid_pos) - boid_vel).limit_length(boid_properties.max_force)
var steer_force := steer * boid_properties.seperation
var align_force := align * boid_properties.alignment
var cohere_force := cohere * boid_properties.cohesion
var target_force := target * boid_properties.targeting
var force := steer_force + align_force + cohere_force + target_force
return force

View File

@ -0,0 +1,17 @@
extends Resource
class_name BoidProperties
## controls the maximum speed.
@export var max_speed := 4.0
## controls the maximum force.
@export var max_force := 1.0
@export_group("weights")
## controls how inclined the boid will be to align with the rest of it's flock.
@export var alignment := 1.5
## controls how inclined the boid will be to cohere together with the rest of it's flock.
@export var cohesion := 1.0
## controls how inclined the boid will be to separate from the rest of it's flock.
@export var seperation := 1.2
## controls how inclined the boid will be to go to a target (defined by a flock).
@export var targeting := 0.8

View File

@ -2,20 +2,23 @@
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="48"
height="24"
viewBox="0 0 48 24.000001"
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="example_boid.svg"
sodipodi:docname="boid.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="#5d6c8e"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
@ -24,11 +27,11 @@
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="px"
showgrid="true"
inkscape:zoom="17.355781"
inkscape:cx="20.886412"
inkscape:cy="8.3833737"
inkscape:window-width="1858"
inkscape:window-height="1057"
inkscape:zoom="34.711562"
inkscape:cx="6.4387768"
inkscape:cy="7.7639837"
inkscape:window-width="1920"
inkscape:window-height="1027"
inkscape:window-x="1912"
inkscape:window-y="-8"
inkscape:window-maximized="1"
@ -39,7 +42,7 @@
originx="0"
originy="0"
spacingx="1"
spacingy="1"
spacingy="1.0000001"
empcolor="#0099e5"
empopacity="0.30196078"
color="#0099e5"
@ -57,9 +60,9 @@
inkscape:groupmode="layer"
id="layer1">
<path
style="fill:#ffffff;stroke-width:22.677;stroke-linejoin:bevel"
d="M 48,12 C 48,-4.9999998e-7 0,0 0,0 c 30,9.9999996 30,14.999999 0,23.999999 0,0 48,0 48,-11.999999 z"
style="fill:#e0e0e0;fill-opacity:1;stroke-width:52.913;stroke-linejoin:bevel"
d="m 1,2.3333337 c 0,0 14,4.6666666 14,5.9464786 C 15,9.3333343 1,14.000001 1,14.000001 5.666666,9.3333343 5.666666,7.0000003 1,2.3333337 Z"
id="path1"
sodipodi:nodetypes="sccs" />
sodipodi:nodetypes="cscc" />
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@ -0,0 +1,38 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://bjix5s2wjg1te"
path="res://.godot/imported/boid_properties.svg-65ee4ea68118f8a485d6b21ab00db988.ctex"
metadata={
"has_editor_variant": true,
"vram_texture": false
}
[deps]
source_file="res://addons/boids/boid_properties/boid_properties.svg"
dest_files=["res://.godot/imported/boid_properties.svg-65ee4ea68118f8a485d6b21ab00db988.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
svg/scale=1.0
editor/scale_with_editor_scale=true
editor/convert_colors_with_editor_theme=true

18
addons/boids/boids.gd Normal file
View File

@ -0,0 +1,18 @@
@tool
extends EditorPlugin
func _enter_tree() -> void:
add_custom_type("BoidProperties", "Resource", preload("boid_properties/boid_properties.gd"), preload("boid_properties/boid_properties.svg"))
add_custom_type("Flock", "Node", preload("flock/flock.gd"), preload("flock/flock.svg"))
add_custom_type("Boid2D", "Node2D", preload("boid_2d/boid_2d.gd"), preload("boid_2d/boid_2d.svg"))
add_custom_type("Boid3D", "Node3D", preload("boid_3d/boid_3d.gd"), preload("boid_3d/boid_3d.svg"))
add_autoload_singleton("BoidManager", "res://addons/boids/boid_manager.gd")
func _exit_tree() -> void:
remove_custom_type("Flock")
remove_custom_type("Boid2D")
remove_custom_type("Boid3D")
remove_custom_type("BoidProperties")
remove_autoload_singleton("BoidManager")

View File

@ -0,0 +1,34 @@
extends Node
class_name Flock
@export var goal_seperation: float = 25.0
@export var goal_alignment: float = 50.0
@export var goal_cohesion: float = 50.0
var boids: Dictionary = {}
## a node that the flock will try to follow.
## target should be either a Node2D or a Node3D (or any inheritors of these two).
@export var target: Node
func _ready() -> void:
self.child_entered_tree.connect(_register_boid)
self.child_exiting_tree.connect(_unregister_boid)
_init_register_boid()
func _init_register_boid(node: Node = self) -> void:
_register_boid(node)
for child: Node in node.get_children():
_init_register_boid(child)
func _register_boid(maybe_boid: Node) -> void:
if maybe_boid is not Boid2D and maybe_boid is not Boid3D: return
maybe_boid.flock = self
boids[maybe_boid.get_instance_id()] = maybe_boid
print_verbose("[", self, "]", " boid ", maybe_boid, " registered")
func _unregister_boid(maybe_boid: Node) -> void:
if maybe_boid is not Boid2D and maybe_boid is not Boid3D: return
boids.erase(maybe_boid.get_instance_id())
print_verbose("[", self, "]", " boid ", maybe_boid, " unregistered")

View File

@ -8,7 +8,7 @@
version="1.1"
id="svg1"
inkscape:version="1.3.2 (091e20e, 2023-11-25, custom)"
sodipodi:docname="boid_2d.svg"
sodipodi:docname="flock.svg"
inkscape:export-filename="..\..\resources\boid_2d.png"
inkscape:export-xdpi="768"
inkscape:export-ydpi="768"
@ -29,9 +29,9 @@
showgrid="true"
inkscape:zoom="24.544781"
inkscape:cx="9.513224"
inkscape:cy="4.0538149"
inkscape:window-width="1858"
inkscape:window-height="1057"
inkscape:cy="7.3131636"
inkscape:window-width="1920"
inkscape:window-height="1027"
inkscape:window-x="1912"
inkscape:window-y="-8"
inkscape:window-maximized="1"
@ -60,17 +60,17 @@
inkscape:groupmode="layer"
id="layer1">
<path
style="fill:#8da5f3;fill-opacity:1;stroke-width:26.4565;stroke-linejoin:bevel"
style="fill:#e0e0e0;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"
style="fill:#e0e0e0;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"
style="fill:#e0e0e0;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" />

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@ -0,0 +1,38 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://b8kh6tdumqytn"
path="res://.godot/imported/flock.svg-e863ec0929f57a6863c3b7914e0d4cd3.ctex"
metadata={
"has_editor_variant": true,
"vram_texture": false
}
[deps]
source_file="res://addons/boids/flock/flock.svg"
dest_files=["res://.godot/imported/flock.svg-e863ec0929f57a6863c3b7914e0d4cd3.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
svg/scale=1.0
editor/scale_with_editor_scale=true
editor/convert_colors_with_editor_theme=true

7
addons/boids/plugin.cfg Normal file
View File

@ -0,0 +1,7 @@
[plugin]
name="Boids"
description="Addon that implements 2D/3D boids / flocking in Godot."
author="yusdacra"
version="0.1"
script="boids.gd"

View File

@ -1,15 +0,0 @@
[gd_scene load_steps=3 format=3 uid="uid://bcyffgnn2ahl3"]
[ext_resource type="PackedScene" uid="uid://bq7s2yf0fohes" path="res://addons/boid_2d/boid.tscn" id="1_825c0"]
[ext_resource type="Texture2D" uid="uid://rk5u1wthr0n0" path="res://examples/boid_2d/example_boid.svg" id="2_qfbgc"]
[node name="Boid" instance=ExtResource("1_825c0")]
collision_layer = 2
[node name="Vision" parent="." index="2"]
collision_mask = 2
[node name="Sprite2D" type="Sprite2D" parent="." index="3"]
position = Vector2(-2.17226e-06, 2.38419e-07)
scale = Vector2(0.111111, 0.111111)
texture = ExtResource("2_qfbgc")

View File

@ -1,12 +0,0 @@
[gd_scene load_steps=2 format=3 uid="uid://op0qicvpbjt6"]
[ext_resource type="Script" path="res://examples/boid_2d/simple/example.gd" id="1_3gcrf"]
[node name="Example" type="Node2D"]
script = ExtResource("1_3gcrf")
[node name="StaticBody2D" type="StaticBody2D" parent="."]
collision_priority = 4.0
[node name="CollisionPolygon2D" type="CollisionPolygon2D" parent="StaticBody2D"]
polygon = PackedVector2Array(1152, 0, 1152, 648, 0, 648, 0, 0, 1088, 0, 1088, 64, 64, 64, 64, 576, 1088, 576, 1088, 0)

View File

@ -0,0 +1,69 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="14"
height="11.666667"
viewBox="0 0 14 11.666668"
version="1.1"
id="svg1"
inkscape:version="1.3.2 (091e20e, 2023-11-25, custom)"
sodipodi:docname="example_boid.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="34.711562"
inkscape:cx="5.4304672"
inkscape:cy="5.4304672"
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="-1"
originy="-2.3333339"
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"
transform="translate(-1,-2.3333337)">
<path
style="fill:#8da5f3;fill-opacity:1;stroke-width:52.913;stroke-linejoin:bevel"
d="m 1,2.3333337 c 0,0 14,4.6666666 14,5.9464786 C 15,9.3333343 1,14.000001 1,14.000001 5.666666,9.3333343 5.666666,7.0000003 1,2.3333337 Z"
id="path1"
sodipodi:nodetypes="cscc" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@ -3,15 +3,15 @@
importer="texture"
type="CompressedTexture2D"
uid="uid://rk5u1wthr0n0"
path="res://.godot/imported/example_boid.svg-ebae3589d3b59182aead052ab0bb5c16.ctex"
path="res://.godot/imported/example_boid.svg-6de905ebc2379a658bac4d710ea0dc0b.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://examples/boid_2d/example_boid.svg"
dest_files=["res://.godot/imported/example_boid.svg-ebae3589d3b59182aead052ab0bb5c16.ctex"]
source_file="res://examples/boids/2d/example_boid.svg"
dest_files=["res://.godot/imported/example_boid.svg-6de905ebc2379a658bac4d710ea0dc0b.ctex"]
[params]
@ -32,6 +32,6 @@ process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
svg/scale=6.0
svg/scale=1.2
editor/scale_with_editor_scale=false
editor/convert_colors_with_editor_theme=false

View File

@ -0,0 +1,34 @@
[gd_scene load_steps=6 format=3 uid="uid://b2sg3n42rkbx8"]
[ext_resource type="Script" path="res://addons/boids/boid_2d/boid_2d.gd" id="1_vh1uc"]
[ext_resource type="Texture2D" uid="uid://rk5u1wthr0n0" path="res://examples/boids/2d/example_boid.svg" id="2_jx2vb"]
[ext_resource type="Script" path="res://addons/boids/boid_properties/boid_properties.gd" id="2_up2nk"]
[sub_resource type="Resource" id="Resource_m74bv"]
script = ExtResource("2_up2nk")
max_speed = 4.0
max_force = 1.0
alignment = 1.5
cohesion = 1.0
seperation = 1.2
targeting = 0.8
[sub_resource type="GDScript" id="GDScript_ldfpo"]
resource_name = "example_boid_sprite"
script/source = "extends Sprite2D
@onready var boid: Boid2D = get_parent()
func _process(delta: float) -> void:
var dir := boid.velocity.normalized()
var target_rot := atan2(dir.y, dir.x)
rotation = move_toward(rotation, target_rot, delta * PI * 2.0 * absf(target_rot - rotation))
"
[node name="ExampleBoid" type="Node2D"]
script = ExtResource("1_vh1uc")
properties = SubResource("Resource_m74bv")
[node name="Sprite2D" type="Sprite2D" parent="."]
texture = ExtResource("2_jx2vb")
script = SubResource("GDScript_ldfpo")

View File

@ -1,18 +1,14 @@
extends Node2D
func _ready() -> void:
for i in 40: spawnBoid()
func _process(delta: float) -> void:
$Path2D/PathFollow2D.progress_ratio += delta * 0.1
func spawnBoid() -> void:
var boid: Boid = preload("../example_boid.tscn").instantiate()
var boid: Boid2D = preload("../example_boid.tscn").instantiate()
var screensize := get_viewport_rect().size
boid.modulate = Color(randf(), randf(), randf(), 1)
boid.global_position = Vector2((randf_range(200, screensize.x - 200)), (randf_range(200, screensize.y - 200)))
boid.follow_point = $Path2D/PathFollow2D
add_child(boid)
$Flock.add_child(boid)

View File

@ -1,6 +1,7 @@
[gd_scene load_steps=3 format=3 uid="uid://ckc0dhvrksfh4"]
[gd_scene load_steps=4 format=3 uid="uid://ckc0dhvrksfh4"]
[ext_resource type="Script" path="res://examples/boid_2d/follow/example.gd" id="1_cb4mx"]
[ext_resource type="Script" path="res://examples/boids/2d/follow/example.gd" id="1_cb4mx"]
[ext_resource type="Script" path="res://addons/boids/flock/flock.gd" id="2_i4bjg"]
[sub_resource type="Curve2D" id="Curve2D_ncwi0"]
_data = {
@ -17,3 +18,7 @@ curve = SubResource("Curve2D_ncwi0")
[node name="PathFollow2D" type="PathFollow2D" parent="Path2D"]
position = Vector2(1117, 34)
rotation = -2.44507
[node name="Flock" type="Node" parent="." node_paths=PackedStringArray("target")]
script = ExtResource("2_i4bjg")
target = NodePath("../Path2D/PathFollow2D")

View File

@ -1,13 +1,12 @@
extends Node2D
func _ready() -> void:
for i in 100: spawnBoid()
for flock in get_children():
for i in 100: spawnBoid(flock)
func spawnBoid() -> void:
var boid: Boid = preload("../example_boid.tscn").instantiate()
func spawnBoid(flock: Flock) -> void:
var boid: Boid2D = preload("../example_boid.tscn").instantiate()
var screensize := get_viewport_rect().size
boid.modulate = Color(randf(), randf(), randf(), 1)
boid.global_position = Vector2((randf_range(200, screensize.x - 200)), (randf_range(200, screensize.y - 200)))
add_child(boid)
flock.add_child(boid)

View File

@ -0,0 +1,22 @@
[gd_scene load_steps=3 format=3 uid="uid://op0qicvpbjt6"]
[ext_resource type="Script" path="res://examples/boids/2d/simple/example.gd" id="1_3gcrf"]
[ext_resource type="Script" path="res://addons/boids/flock/flock.gd" id="2_1xeeb"]
[node name="Example" type="Node2D"]
script = ExtResource("1_3gcrf")
[node name="Flock" type="Node" parent="."]
script = ExtResource("2_1xeeb")
[node name="Flock2" type="Node" parent="."]
script = ExtResource("2_1xeeb")
[node name="Flock3" type="Node" parent="."]
script = ExtResource("2_1xeeb")
[node name="Flock4" type="Node" parent="."]
script = ExtResource("2_1xeeb")
[node name="Flock5" type="Node" parent="."]
script = ExtResource("2_1xeeb")

View File

@ -11,14 +11,31 @@ config_version=5
[application]
config/name="Boid Addon"
run/main_scene="res://examples/boid_2d/simple/example.tscn"
run/main_scene="res://examples/boids/2d/simple/example.tscn"
config/features=PackedStringArray("4.3", "GL Compatibility")
[autoload]
BoidManager="*res://addons/boids/boid_manager.gd"
[debug]
settings/stdout/verbose_stdout=true
[editor_plugins]
enabled=PackedStringArray("res://addons/boid_2d/plugin.cfg")
enabled=PackedStringArray("res://addons/boids/plugin.cfg")
[physics]
2d/run_on_separate_thread=true
3d/run_on_separate_thread=true
[rendering]
renderer/rendering_method="gl_compatibility"
renderer/rendering_method.mobile="gl_compatibility"
[threading]
worker_pool/max_threads=20