# Boids - A Shoes Application
#
# Author : Wally Glutton - http://stungeye.com
# Summary : A hungry swarm indeed!
#
# Notes : My home-rolled Vector class appears to be quicker than the matrix library Vectors.
# Boid Algo : http://www.vergenet.net/~conrad/boids/pseudocode.html
#
# Required : You must have Shoes installed to view the boids.
# Get Shoes : http://code.whytheluckystiff.net/shoes/
# Learn Shoes : http://hackety.org/press/nks.html
#
# Code License : http://creativecommons.org/licenses/by-sa/2.5/ca/
srand
NUM_BOIDS = 30
NUM_FOODSTUFF = 6
boids = []
foodstuff = []
Shoes.app do
stroke rgb(0x30, 0x30, 0x05, 0.5)
app = self
NUM_BOIDS.times { |i| boids[i] = Boid.new rand * self.width, rand * self.height, random(-5, 5), random(-5, 5) }
NUM_FOODSTUFF.times { |i| foodstuff[i] = Food.new app}
animate(24) do
clear do
boids.each do |boid|
boid.calculate_avoidance_delta boids # Avoid other boids
boid.calculate_attraction_delta boids # Gravitate towards the centre-of-mass of nearby boids
boid.calculate_allignment_delta boids # Allign velocity with nearby boids
boid.calculate_hunting_delta foodstuff # Be on the lookout for food
boid.calculate_stay_visible_delta self # Don't fly too far from home
boid.apply_deltas
boid.limit_speed
boid.move
boid.draw self, app
foodstuff.each do |food|
food.eaten? boid
end
end
foodstuff.each do |food|
food.draw self
end
end
end
end
class Food
RADIUS = 30
attr_reader :position
def initialize app
@app = app
spawn
end
def spawn
@size = RADIUS
@position = VectorK.new rand * @app.width, rand * @app.height
end
def eaten? boid
if @position.nearby? RADIUS, boid.position
@size -= 1
end
if @size < 0
spawn
end
end
def draw slot
@app.fill rgb(0xFF, 0x30, 0xFF, 0.4)
slot.oval :left => @position.x, :top => @position.y, :radius => @size, :center => true
end
end
class Boid
RADIUS = 20
MAX_SPEED = 25
AVOID_RADIUS = RADIUS*3 # Avoid other boids within this radius
AVOID_DAMPER = 100
ATTRACTION_RADIUS = RADIUS*8 # Gravitate to the centre of mass of boids within this radius
ATTRACTION_DAMPER = 30
ALLIGNMENT_RADIUS = RADIUS*3 # Allign velocity with boids within this radius
ALLIGNMENT_DAMPER = 30
HUNTING_RADIUS = RADIUS*5 # Locate food within this radius
HUNTUNG_DAMPER = 10
STAY_VISIBLE_DAMPER = 300
attr_reader :velocity, :position
def initialize x, y, vx, vy
@velocity = VectorK.new vx, vy
@position = VectorK.new x, y
@velocity_delta = VectorK.new 0, 0
end
def calculate_avoidance_delta boids
boids.each do |other|
if @position.nearby? AVOID_RADIUS, other.position
@velocity_delta += (@position - other.position) / AVOID_DAMPER
end
end
end
def calculate_attraction_delta boids
average_position = VectorK.new 0, 0
visible_boids = 0
boids.each do |other|
if @position.nearby? ATTRACTION_RADIUS, other.position
average_position += other.position
visible_boids += 1
end
end
average_position /= visible_boids
@velocity_delta += (average_position - @position) / ATTRACTION_DAMPER
end
def calculate_allignment_delta boids
allignment_delta = VectorK.new 0, 0
visible_boids = 0
boids.each do |other|
if @position.nearby? ALLIGNMENT_RADIUS, other.position
allignment_delta += other.velocity
visible_boids += 1
end
end
allignment_delta /= visible_boids
@velocity_delta += allignment_delta / ALLIGNMENT_DAMPER
end
def calculate_hunting_delta foodstuff
foodstuff.each do |food|
if @position.nearby? HUNTING_RADIUS, food.position
@velocity_delta += (food.position - @position) / HUNTUNG_DAMPER
end
end
end
def calculate_stay_visible_delta slot
mid_x = slot.width / 2
mid_y = slot.height / 2
@velocity_delta -= (@position - VectorK.new(mid_x, mid_y)) / STAY_VISIBLE_DAMPER
end
def apply_deltas
@velocity += @velocity_delta
@velocity_delta = VectorK.new 0, 0
end
def limit_speed
if @velocity.r > MAX_SPEED
@velocity /= @velocity.r # Create a unit vector
@velocity *= MAX_SPEED # Scale to max speed
end
end
def move
@position += @velocity
end
def draw slot, app
app.fill rgb(0x30, 0xFF, 0xFF, 0.5)
slot.oval :left => @position.x, :top => @position.y, :radius => RADIUS, :center => true
slot.line @position.x, @position.y, (@position.x + @velocity.x), (@position.y + @velocity.y)
end
end
class VectorK
attr_reader :x, :y
def initialize x, y
@x = x
@y = y
end
def nearby? threshold, a
return false if a === self
(distance a) < threshold
end
def distance a
Math.sqrt((@x - a.x)**2 + (@y - a.y)**2)
end
def / a
if (a != 0)
VectorK.new(@x / a, @y / a)
else
self
end
end
def + a
VectorK.new(@x + a.x, @y + a.y)
end
def - a
VectorK.new(@x - a.x, @y - a.y)
end
def * a
VectorK.new(@x * a, @y * a)
end
def r
Math.sqrt(@x * @x + @y * @y)
end
end
def random min, max
choice = (max - min > 0) ? (rand max - min) + min : 0;
end
Refactorings
No refactoring yet !
You will need Shoes installed to run this sketch: http://code.whytheluckystiff.net/shoes/
I'm looking for speed optimizations and a reduction in code-size (but not at the expense of legibility).
Also, coming from a C/Javascript background, I'd appreciate suggestions that change my code to better follow the 'Ruby way'.
Tested with Shoes 0.r396, Windows XP.
Thank you kindly.