godot-boids-experiments/scripts/BoidsManager.gd

203 lines
6.2 KiB
GDScript

extends Node3D
@export var resources : ResourcePreloader
@export_subgroup("Initialisation")
@export var start_count : int = 10
@export var spawn_volume : Vector3 = Vector3.ONE
@export_subgroup("Boid settings")
@export var protected_range : float = 1.0
@export var visual_range : float = 2.0
@export var centering_factor : float = 0.0005
@export var avoid_factor : float = 0.05
@export var matching_factor : float = 0.05
@export var min_speed : float = 2.0
@export var max_speed : float = 3.0
@export var turn_factor : float = 0.2
@export var margin : float = 0.3
@export_subgroup("Edges")
@export var shape : Shape3D
@export_subgroup("Gravity")
@export var use_gravity : bool = true
@export var gravity_vector : Vector3 = Vector3(0, -9.807, 0)
@export var gravity_factor : float = 0.1
var boids : Array[Boid]
var boid_scene : PackedScene
var protected_range_squared : float
var visual_range_squared : float
# Called when the node enters the scene tree for the first time.
func _ready():
protected_range_squared = pow(protected_range, 2)
visual_range_squared = pow(visual_range, 2)
boid_scene = resources.get_resource("Boid")
for i in range(start_count):
var boid : Boid = boid_scene.instantiate()
boid.position = random_pos()
boid.velocity = random_vel()
add_child(boid)
boids.push_back(boid)
# Called every frame. 'delta' is the elapsed time since the previous frame.
func _process(delta):
for boid in boids:
var neighboring_boids : int = 0
var close : Vector3 = Vector3.ZERO
var position_avg : Vector3 = Vector3.ZERO
var velocity_avg : Vector3 = Vector3.ZERO
for other_boid in boids.filter(func(cur): return cur != boid):
# Micro optimization, don't compute distance yet, just check if it's in the range by checking
# individual dimension distances
var diff : Vector3 = other_boid.position - boid.position
if abs(diff.x) < visual_range and abs(diff.y) < visual_range and abs(diff.z) < visual_range:
# Using the squared distance so it performs better
var squared_distance : float = boid.position.distance_squared_to(other_boid.position)
# If other boid too close, add the difference (if multiple boids too close, it should average naturally)
if squared_distance < protected_range_squared:
close -= diff
# If other boid in visual range, we add its position and velocity
elif squared_distance < visual_range_squared:
position_avg += other_boid.position
velocity_avg += other_boid.velocity
neighboring_boids += 1
# We collected all data related to neighboring boids
# Let's change our boid behavior
if neighboring_boids > 0:
position_avg /= neighboring_boids
velocity_avg /= neighboring_boids
boid.velocity = (boid.velocity +
(position_avg - boid.position) * centering_factor +
(velocity_avg - boid.velocity) * matching_factor
)
# Add avoidance contribution to velocity
boid.velocity = boid.velocity + (close * avoid_factor)
# Edge avoidance
boid.velocity = apply_edge_avoidance(boid.position, boid.velocity)
boid.velocity = apply_gravity(boid.velocity, delta)
boid.velocity = apply_speed_limits(boid.velocity)
# Update position, finally!
boid.position += boid.velocity * delta
boid.look_at(boid.global_position + boid.velocity.normalized())
func apply_edge_avoidance(position : Vector3, velocity : Vector3) -> Vector3:
if shape is BoxShape3D:
velocity = apply_cube_edge_avoidance(position, velocity, shape as BoxShape3D)
elif shape is CylinderShape3D:
velocity = apply_cylinder_edge_avoidance(position, velocity, shape as CylinderShape3D)
elif shape is SphereShape3D:
velocity = apply_sphere_edge_avoidance(position, velocity, shape as SphereShape3D)
else:
print("Shape not implemented!")
return velocity
func apply_cube_edge_avoidance(position : Vector3, velocity : Vector3, box : BoxShape3D) -> Vector3:
if position.x < (-box.size.x / 2.0) + margin:
velocity.x += turn_factor
if position.x > (box.size.x / 2.0) - margin:
velocity.x -= turn_factor
if position.y < (-box.size.y / 2.0) + margin:
velocity.y += turn_factor
if position.y > (box.size.y / 2.0) - margin:
velocity.y -= turn_factor
if position.z < (-box.size.z / 2.0) + margin:
velocity.z += turn_factor
if position.z > (box.size.z / 2.0) - margin:
velocity.z -= turn_factor
return velocity
func apply_cylinder_edge_avoidance(position : Vector3, velocity : Vector3, cylinder : CylinderShape3D) -> Vector3:
if position.y < (-cylinder.height / 2.0) + margin:
velocity.y += turn_factor
if position.y > (cylinder.height / 2.0) - margin:
velocity.y -= turn_factor
# We're centering around zero so let's take the position as direction
var direction : Vector2 = Vector2(position.x, position.z)
if direction.length() > cylinder.radius - margin:
var turn : Vector2 = -direction.normalized() * turn_factor
velocity.x += turn.x
velocity.z += turn.y
return velocity
func apply_sphere_edge_avoidance(position : Vector3, velocity : Vector3, sphere : SphereShape3D) -> Vector3:
var direction : Vector3 = position
if direction.length() > sphere.radius - margin:
velocity = -direction.normalized() * turn_factor
return velocity
func apply_gravity(velocity : Vector3, delta : float) -> Vector3:
if use_gravity:
velocity += gravity_vector * gravity_factor * delta
return velocity
func apply_speed_limits(velocity : Vector3) -> Vector3:
# Boid's speed
var speed : float = velocity.length()
# Enforce max and min speed
if speed < min_speed:
velocity = (velocity / speed) * min_speed
if speed > max_speed:
velocity = (velocity / speed) * max_speed
return velocity
func random_pos() -> Vector3:
var x_range : Vector2 = Vector2(-spawn_volume.x / 2.0, spawn_volume.x / 2.0)
var y_range : Vector2 = Vector2(-spawn_volume.y / 2.0, spawn_volume.y / 2.0)
var z_range : Vector2 = Vector2(-spawn_volume.z / 2.0, spawn_volume.z / 2.0)
return Vector3(
randf_range(x_range.x, x_range.y),
randf_range(y_range.x, y_range.y),
randf_range(z_range.x, z_range.y)
)
func random_vel() -> Vector3:
var speed : float = pow(randf_range(min_speed, max_speed), 2)
var x : float = randf_range(0, speed)
speed -= x
var y : float = randf_range(0, speed)
speed -= y
var z : float = randf_range(0, speed)
return Vector3(sqrt(x), sqrt(y), sqrt(z))