Last active
April 16, 2022 01:39
-
-
Save SebastianPoell/9657653504f467c811e03b824554e6aa to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# This script simulates flocking behavior of boids (bird-oid objects), based | |
# on Craig Reynolds algorithm. Also inspired by: https://eater.net/boids. | |
# It could probably be better optimized for performance, e.g. by using | |
# Quadtrees. Utilizing the 'matrix' library for vector operations didn't | |
# seem to improve speed. | |
# | |
# For now, this implementation: | |
# - Is independent of any drawing library | |
# - Fully parameterizes flocks | |
# - Supports simulating multiple independent flocks | |
# - Supports edges for restricting boids | |
# - Supports attractor / repeller points | |
# | |
class Boid | |
attr_accessor :x, :y, :vx, :vy | |
def initialize(x, y, max_speed) | |
@x, @y, @max_speed = x, y, max_speed | |
@vx = rand(-max_speed..max_speed) | |
@vy = rand(-max_speed..max_speed) | |
end | |
# Every boid wants to align its velocity to other boids | |
def delta_alignment(boids, radius, weight) | |
delta_x, delta_y = 0, 0 | |
neighbors = neighbors(boids, radius) | |
neighbors.each do |boid| | |
delta_x += boid.vx | |
delta_y += boid.vy | |
end | |
unless neighbors.empty? | |
delta_x = delta_x / neighbors.size - @vx | |
delta_y = delta_y / neighbors.size - @vy | |
end | |
[delta_x * weight, delta_y * weight] | |
end | |
# Every boid wants to be close to other boids | |
def delta_cohesion(boids, radius, weight) | |
delta_x, delta_y = 0, 0 | |
neighbors = neighbors(boids, radius) | |
neighbors.each do |boid| | |
delta_x += boid.x | |
delta_y += boid.y | |
end | |
unless neighbors.empty? | |
delta_x = delta_x / neighbors.size - @x | |
delta_y = delta_y / neighbors.size - @y | |
end | |
[delta_x * weight, delta_y * weight] | |
end | |
# Every boid doesn't want to collide with other boids | |
def delta_separation(boids, radius, weight) | |
delta_x, delta_y = 0, 0 | |
neighbors = neighbors(boids, radius) | |
neighbors.each do |boid| | |
delta_x += @x - boid.x | |
delta_y += @y - boid.y | |
end | |
[delta_x * weight, delta_y * weight] | |
end | |
# A boid doesn't want to collide with edges | |
def delta_edges(width, height, margin, weight) | |
delta_x = case | |
when @x < margin then 1 | |
when @x > width - margin then -1 | |
else 0 | |
end | |
delta_y = case | |
when @y < margin then 1 | |
when @y > height - margin then -1 | |
else 0 | |
end | |
[delta_x * weight, delta_y, * weight] | |
end | |
# Attracts or repells boids (depending on the sign of the weight) | |
def delta_attractor(attractor_x, attractor_y, weight) | |
return [0, 0] unless attractor_x && attractor_y | |
delta_x = attractor_x - @x | |
delta_y = attractor_y - @y | |
[delta_x * weight, delta_y * weight] | |
end | |
# Overwrites setter for x velocity and limits speed of boid | |
def vx=(vx) | |
@vx = vx | |
speed = Math.sqrt((@vx ** 2) + (@vy ** 2)) | |
@vx = (@vx / speed) * @max_speed if speed > @max_speed | |
end | |
# Overwrites setter for y velocity and limits speed of boid | |
def vy=(vy) | |
@vy = vy | |
speed = Math.sqrt((@vx ** 2) + (@vy ** 2)) | |
@vy = (@vy / speed) * @max_speed if speed > @max_speed | |
end | |
private | |
def neighbors(boids, radius) | |
(boids - [self]).select { |b| distance(b) < radius } | |
end | |
def distance(boid) | |
Math.sqrt((@x - boid.x) ** 2 + (@y - boid.y) ** 2) | |
end | |
end | |
class Flock | |
attr_accessor :boids | |
def initialize(opts = {}) | |
# Merge arguments with defaults and set them as instance variables | |
{ | |
edge_width: 1300, | |
edge_height: 700, | |
number_of_boids: 100, | |
boid_max_speed: 15, | |
alignment_radius: 75, | |
alignment_weight: 0.05, | |
cohesion_radius: 75, | |
cohesion_weight: 0.005, | |
separation_radius: 20, | |
separation_weight: 0.05, | |
edges_margin: 200, | |
edges_weight: 1 | |
}.merge(opts).each do |key, value| | |
instance_variable_set("@#{key}", value) | |
end | |
# Init boids for this flock | |
@boids = @number_of_boids.times.map do | |
Boid.new(rand(@edge_width), rand(@edge_height), @boid_max_speed) | |
end | |
end | |
# Sets an attractor or repeller point (e.g. by clicking). | |
# Could be easily changed to support arrays of attractors. | |
def set_attractor(x, y, weight) | |
@attractor_x, @attractor_y, @attractor_weight = x, y, weight | |
end | |
# Moves all boids of this flock | |
def move | |
@boids.each do |boid| | |
# Calculate all influences for this boid | |
ax, ay = boid.delta_alignment( | |
@boids, @alignment_radius, @alignment_weight | |
) | |
cx, cy = boid.delta_cohesion( | |
@boids, @cohesion_radius, @cohesion_weight | |
) | |
sx, sy = boid.delta_separation( | |
@boids, @separation_radius, @separation_weight | |
) | |
ex, ey = boid.delta_edges( | |
@edge_width, @edge_height, @edges_margin, @edges_weight | |
) | |
tx, ty = boid.delta_attractor( | |
@attractor_x, @attractor_y, @attractor_weight | |
) | |
# Sum up influences and adapt velocity | |
boid.vx += ax + cx + sx + ex + tx | |
boid.vy += ay + cy + sy + ey + ty | |
# Move position accordingly to velocity vector | |
boid.x += boid.vx | |
boid.y += boid.vy | |
end | |
end | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Use it like that:
Or use it with ruby2d:
flocks.mov