Skip to content

Instantly share code, notes, and snippets.

@SebastianPoell
Last active April 16, 2022 01:39
Show Gist options
  • Save SebastianPoell/9657653504f467c811e03b824554e6aa to your computer and use it in GitHub Desktop.
Save SebastianPoell/9657653504f467c811e03b824554e6aa to your computer and use it in GitHub Desktop.
# 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
@SebastianPoell
Copy link
Author

SebastianPoell commented Feb 12, 2021

Use it like that:

# Create a flock and move boids
flock = Flock.new(number_of_boids: 100)
1000.times do
  flock.move
  puts ("(#{flock.boids.first.x}, #{flock.boids.first.y})")
end

Or use it with ruby2d:

flocks.mov
# Use ruby2d library for drawing
require "ruby2d"

# Set the window size
set width: 1300, height: 700

# Init flocks
flocks = [
  Flock.new(number_of_boids: 50),
  Flock.new(number_of_boids: 50)
]

# Add accessor for image to boid
Boid.module_eval { attr_accessor :img }

# Set image for each boid
flocks.each_with_index do |flock, index|
  flock.boids.each do |boid|
    boid.img = Image.new(
      "triangle_#{index}.png", width: 30, height: 30
    )
  end
end

# Register event handler to attract or repell boids
Ruby2D::Window::on :mouse_down do |event|
  flocks.each do |flock|
    weight = case event.button
      when :left then 0.01
      when :right then -0.01
    end
    flock.set_attractor(event.x, event.y, weight)
  end
end

# Register event handler to reset attractor / repeller
Ruby2D::Window::on :mouse_up do |event|
  flocks.each do |flock|
    flock.set_attractor(nil, nil, 0)
  end
end

# Indefinitely move boids and move / rotate all images accordingly
# Always make sure the boid in your image is pointing to the right (= 0 degrees)
# Alternatively you can simply add degrees to the rotate attribute
Ruby2D::Window::update do
  flocks.each do |flock|
    flock.move
    flock.boids.each do |boid|
      boid.img.x = boid.x
      boid.img.y = boid.y
      boid.img.rotate = Math.atan2(boid.vy, boid.vx) * 180 / Math::PI
    end
  end
end

# Render window
Ruby2D::Window::show

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment