XIVPS

A blog concerned with electronics and everything else

BOIDS

14 Oct 2019 - val

What are boids? Boids are approximations for birds, fish and other animals (or things, really) exhibiting flocking behaviour, and are used for simulating just that. Boids behave according to certain rules. These are as follows:

I will implement this behaviour using Haskell, with gloss as the renderer. Now, let’s start by implementing a data structure.

The data structure

type Vector = (Float, Float)

data Boid = Boid { position     :: (Float, Float)
                 , velocity     :: (Float, Float)
                 , acceleration :: (Float, Float)
                 }

At first, I define a type alias Vector, which is really just a pair of 2 Numbers when working in 2D. I use this type to describe the movements of my boids.

Right now, we only store the position, velocity and acceleration of a boid (all as vectors), but I’m not sure we actually need to store acceleration. I am wondering because acceleration is the quantity we will be calculating based on the boids surroundings.

The data structure itself is written in record syntax. Check it out here if you have never seen it before. Next, I assign aliases to the getter functions generated by the record, for brevity’s sake.

Renderer

Now that we can store boids, lets display them!

DRAWING A BOID

We will display a boid as a simple triangular shape.

baseTriangle :: Picture
baseTriangle = Polygon [(8, 0), (-4, 4), (-3, 0), (-4, -4)]

drawBoid :: Boid -> Picture
drawBoid (Boid (x, y) _ a) = translate x y $ rotate angle $ color white baseTriangle
  where
    angle = (360 / (2 * pi)) * phase ihat a

drawBoids :: [Boid] -> Picture
drawBoids = pictures . map drawBoid

ihat :: Vector
jhat :: Vector
ihat = (1, 0)
jhat = (0, 1)

In order to test our function, we need a way to generate a bunch of Boids at once. At least, I wont type them out.

testBoids :: Float -> [Boid]
testBoids n = [makeBoidAtPos (x*10, y*10) | x <- [1..m], y <- [1..m] ]
  where
    m = n / 10

This fuction generates a grid of boids form (0, 0) to (n, n). Now lets feed that into drawBoids.

display FullScreen black $ translate (-960) (-540) $ drawBoids $ testBoids 100

Wonderful! We can see all our little boids, bunched up in the corner of the screen. If we zoom in, we can see the shape we defined. Also, we can clearly see that all boids face in the same direction, which makes sense, as all boids are generated with an acceleration of 0.

pos :: Boid -> Vector
pos = position

vel :: Boid -> Vector
vel = velocity

acc :: Boid -> Vector
acc = acceleration

Sticky

The rule of cohesion
The rule of cohesion (Source)

The rule of cohesion says that boids tend to cluster together. More accurately, the tend to follow the “center of mass” of their peers. This means that in order to implement cohesion, we need a function that can find the “center of mass”. Let’s call it findCenter. How creative.

MAN IN THE MIDDLE

This is how I implemented findCenter:

findCenter :: [Boid] -> Vector
findCenter boids = sum /> len
  where
    sum = (sumv . map pos) boids
    len = (fromIntegral . length) boids

Ok… and what does this do? Let’s start with the the two variables, sum and len.

sum is just the sum of all the position vectors of the boids that were passed to the function, while len is the amount of boids. In order to calculate sum, I implemented a new function sumv to sum vectors, with the self-defined +>, an operator to add two vectors.

sumv :: [Vector] -> Vector
sumv = foldl (+>) (0, 0)

(+>) :: Vector -> Vector -> Vector
(a, b) +> (c, d) = (a+c, b+d)

The last unknown is the /> operator. It divides any vector by a scalar.

(/>) :: Vector -> Float -> Vector
(a, b) /> c = (a/c, b/c)

COHESION

Now, how do we get our boid to actually move towards the center of mass of it and its peers? Simple. We subtract our the position vector of findCenter form our own (the boids own) position vector. This is the direction we want to move in, according to the rule of cohesion. ~> is simply vector subtraction.

cohesion :: Boid -> [Boid] -> Vector
cohesion b bs = findCenter bs ~> pos b

(~>) :: Vector -> Vector -> Vector
(a, b) ~> (c, d) = (a-c, b-d)
A boids field of view
A boids field of view (Source)

This should work fine, but in order for proper swarm-like behaviour to emerge, we can’t have our boids seeing every other boid. Therefore, we need a maximum view distance as well as a view angle – a field of view.

So we change out cohesion function like this:

cohesion :: Boid -> [Boid] -> Vector
cohesion b bs = findCenter peers ~> pos b
  where
    peers = filter (canView b) bs

Simple enough, right? Well, the entire complexity is hiding behind canView.

viewDist :: Float
viewDist = 100

viewAngleDeg :: Float -- from center to boder in degrees
viewAngleDeg = 70

viewAngleRad :: Float
viewAngleRad = viewAngleDeg * 2 * pi / 360

dist :: Vector -> Vector -> Float
dist (a, b) (c, d) = sqrt $ dx^2 + dy^2
  where
    dx = a - c
    dy = b - d

phase :: Vector -> Vector -> Float
phase u v = acos $ dotp / len
  where
    dotp = u .> v
    len = (lenv u) * (lenv v)

canView :: Boid -> Boid -> Bool
canView viewer subject
  | dist (pos viewer) (pos subject) > viewDist = False
  | abs ( phase (acc viewer) (pos subject ~> pos viewer) ) > viewAngleRad = False
  | otherwise = True

dist and phase can calculate the distance between two position vectors, or the pahse between two vectors, respectively. We need these to check whether or not a boid is withing our viewing range and field of view. Checking whether another boid is close enough to influence us is relatively simple – just perform dist on our position vectors. Checking for FOV is a bit harder though – we know that we need to calculate phase, but on what vectors?

The first vector should be our heading. To get that, we will just look at the acceleration of our boid (meaning that we actually do have to store it). The other vector should be the vector between us and the other boid. So we use our getter functions to access the relevant vectors and calculate away! By the way, .> is just a dot product operator.

Icky

On to the rule of separation!

SAFE SPACE

Every boid wants to keep a certain distance from other boids in order to avoid crashing. This is why we need a way to get all boids within our personal “safe space”.

boidsInSafeSpace :: Boid -> [Boid] -> [Boid]
boidsInSafeSpace b bs = filter (inSafeSpace b) bs
  where
    inSafeSpace subject suspect = dist (pos subject) (pos suspect) > safeSpace

GET AWAY!

Now that we know who is getting to close, we can figure out where to run to. Let’s write a function that takes two boids and returns a desired direction of movement.

escapeRoute :: Boid -> Boid -> Vector
escapeRoute escaper intruder = route /> (distance / safeSpace)
  where
    route = pos escaper ~> pos intruder
    distance = lenv route

We simply calculate the vector between the two boid position and scale it according to the distance between the boids – the closer another boid is, the faster a boid wants to get away.

SEPARATION

Using our new escapeRoute function, we can now implement seperation. At first, we get all boids inside our safe space, run escapeRoute on each one, and add up the escape vectors.

seperation :: Boid -> [Boid] -> Vector
seperation b = normalize . foldl (\ vector boid -> vector +> escapeRoute b boid) (0, 0)

Alignment

Last but not least – Alignment. I’ll make this quick. For this rule, we can reuse the canView function. The whole alignment calculation is pretty similar to the cohesion calculation. Instead of averaging positions, velocities are averaged. Then, we do not need to subtract anything, because we will just steer in that same direction.

alignment :: Boid -> [Boid] -> Vector
alignment boid boids = normalize $ sum /> len
  where
    peers = filter (canView boid) boids
    sum = (sumv . map vel) peers
    len = (fromIntegral . length) peers