|
|
# Hungry Boids v0.4 - A Shoes Application
#
# Author : Wally Glutton - http://stungeye.com
# Summary : A hungry swarm indeed!
#
# Boid Algo : http://www.vergenet.net/~conrad/boids/pseudocode.html
# Notes : My home-rolled Vector class appears to be quicker than the matrix library Vectors.
#
# 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 : Creative Commons Attribution-Share Alike 2.5
# License URL : http://creativecommons.org/licenses/by-sa/2.5/ca/
#
# Change Log : v0.1 - The initial swarm.
# v0.2 - Replaced custom random method with (min..max).rand
# - Mouse clicks add food. Careful not to add too much.
# Magnus Adamsson v0.3 - Seems Shoes 0.r646 can no longer draw a zero radius circle
# Wally Glutton v0.4 - No longer using a $app global to access Shoes methods within the Food and Boid classes.
# - Moved class definitions above Shoes.app block as this was causing a problem for OSX builds.
srand
NUM_BOIDS = 25
NUM_FOODSTUFF = 5
boids = []
foodstuff = []
class Food
RADIUS = 30
attr_reader :position
def initialize (app, x=(rand*app.width), y=(rand*app.height))
spawn x, y
@app = app
end
def spawn (x=(rand*@app.width), y=(rand*@app.height))
@size = RADIUS
@position = VectorK.new x, y
end
def eaten? boid
if @position.nearby? RADIUS, boid.position
@size -= 1
end
if @size <= 0
spawn
end
end
def draw
@app.fill rgb(0xFF, 0x30, 0xFF, 0.4)
@app.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 = 50
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 = 50
HUNTING_RADIUS = RADIUS*5 # Locate food within this radius
HUNTUNG_DAMPER = 10
STAY_VISIBLE_DAMPER = 450
attr_reader :velocity, :position
def initialize x, y, vx, vy, app
@velocity = VectorK.new vx, vy
@position = VectorK.new x, y
@velocity_delta = VectorK.new 0, 0
@app = app
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
mid_x = @app.width / 2
mid_y = @app.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
@app.fill rgb(0x30, 0xFF, 0xFF, 0.5)
@app.oval :left => @position.x, :top => @position.y, :radius => RADIUS, :center => true
@app.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
Shoes.app do
stroke rgb(0x30, 0x30, 0x05, 0.5)
NUM_BOIDS.times { |i| boids[i] = Boid.new(rand * self.width, rand * self.height, (-15..15).rand, (-15..15).rand, self) }
NUM_FOODSTUFF.times { |i| foodstuff[i] = Food.new(self) }
click do |_,x,y|
foodstuff.push Food.new(self, x, y)
end
animate(24) do
clear do
background rgb(0xFF, 0xFF, 0xFF)
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 # Don't fly too far from home
boid.apply_deltas
boid.limit_speed
boid.move
boid.draw
foodstuff.each do |food|
food.eaten? boid
end
end
foodstuff.each do |food|
food.draw
end
end
end
end
|