godot_boids/addons/boid_2d/boid.gd
2024-08-19 16:53:54 +03:00

122 lines
3.8 KiB
GDScript

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