Boids Simulation: Part 3
Our boids are currently moving around with a fixed velocity. The next step towards implementing proper boid movement is to provide a mechanism for the boids to be aware of their neighbours’ positions and velocities.
To do this we will add another process, called a space cell, that will keep track of all the boids’ positions+velocities and relay them to other boids. We will connect each boid to the cell with two channels — one for the boid to send its new position+velocity, and one for the cell to send the boid that same information about its neighbours. To get the new version explained in this post, use the command:
darcs get --tag p3b http://patch-tag.com/r/twistedsquare/chp-boids chp-boids-p3b
The ordering for these messages is fairly straightforward. Each frame of movement (borrowing an animation term) will proceed like this:
- The boids send their new position+velocity to the cell.
- The cell sends the drawing process the current boid positions+velocities.
- The cell sends neighbour positions+velocities to all the boids.
- The boids each calculate their new velocity and thus their new position.
This is illustrated below — the arrows are channels (dashed blue when they are being communicated on, black otherwise) connecting up our boids, the cell and the display:
Here is the new cell process:
cell :: [(Chanout [BoidInfo], Chanin BoidInfo)] -> Chanout [BoidInfo] -> CHP () cell chanPairs outputPos = forever cell' where (outputs, inputs) = unzip chanPairs cell' = do pos <- mapM readChannel inputs writeChannel outputPos pos zipWithM_ writeChannel outputs (nearby 0.05 pos)
The first parameter to the cell is the list of channel pairs (one per boid); the second parameter is the outgoing channel to the drawing process. The main body of the process is quite simple. It reads boid information from all the channels, then sends it on to the drawing process. After that, it sends back to the boids all the information about boids near them. The nearby function has this type:
nearby :: Float -> [BoidInfo] -> [[BoidInfo]]
The first parameter is a threshold distance at which to filter boids. The second parameter is the list of boid positions+velocities. The return is a list such that the first entry is the list of all boids near to the first boid in the supplied list (except for the first boid itself), the second item in the return list is the list of all boids near to the second boid in the supplied list (but not the second boid) and so on. Thus we can zip the in-order list of boid channels with the in-order list of boid neighbours in the cell’ process.
At the moment, our boid is not implementing the full range of boid rules — only the rule that makes it adopt the mean velocity of its neighbours. That was done fairly simply:
boid' cur = do writeChannel out cur neighbours <- readChannel input -- Use average velocity of nearby boids for now: let average xs = sum xs / fromIntegral (length xs) (vX, vY) | null neighbours = (defaultVelX, defaultVelY) | otherwise = (average $ map velX neighbours ,average $ map velY neighbours) boid' $ BoidInfo (clamp $ posX cur + vX) (clamp $ posY cur + vY) vX vY
The boid first sends out its position, then it reads in the information about its neighbours. The let block contains the pure calculation for adopting the mean velocity of its neighbours, and then the boid recurses.
If you run the current version of the simulation, you will see a coagulating effect occur — as the boids near each other, they slowly form into a static mass that then moves slowly along as one (see the example screenshot on the right). This is not full flocking behaviour, but it at least bears a resemblance! In the next part of the guide we will implement the full set of boid rules.