Boids in Unity

Uploaded at 12.2020
Estimated Time ~30 min
Used tools
  • Unity 2020.1.6f1
Skills required
  • C# Programming Skills
  • Familiarity with Unity
Tutorial Code GitHub Repo

I recently learned about the concept of Boids, a neat little idea from the 80s by Craig Reynolds. Basically he abstracted the behaviour of birds into so called “boids” or “bird-oid objects” by defining 3 simple yet powerful rules. You can think of these rules as a sort of “instinct” that each Boid will follow at all times. Applying these rules in game development yields a very realistic simulation of swarm behaviour, without too much code. Similar to the Game of Life by John Conway simple rules create a very complex pattern of results. I really like the idea of complexity arising from simplicity and I feel like Boids have a huge potential to create more than just birds. With this in mind I’d like to share on how to implement the basic concept of birds in your own project. We will be using Unity 2020 and C#. The final code will also be linked at the end of the tutorial.

So how do Boids work?

Well first of all they are not a magic solution to simulate *everything* a bird would do in real life. No. Boids only simulate the behaviour of movement in relation to other herd creatures. So a Boid can’t land and will never fatigue. Nothing will be auto detected unless we explicitly define it as a rule or instinct for our Boid. Long story short: At the start of this tutorial each Boid is an entity that has a position 3D space (x,y and z coordinates) and a direction of movement, also in 3D. This obviously means we will implement a 3D version for Boids, while the base concept is actually in 2D. The good thing is that we can apply the concept pretty easily in a 3D space. But more on that later. First we need to set up our starting environment.

Setup

Open up your Unity project and create a new scene. Create a Prefab for your Boid using a Capsule or any other custom model. Make sure that your Boid Prefab has no collider in itself or its children. Finally create a new Script called “BoidController” and add it to your Boid Prefab. We will use it to control each individual Boid. You can copy the following reference code for a quick setup.

public class BoidController : MonoBehaviour
{
    public int SwarmIndex { get; set; }
    public float NoClumpingRadius { get; set; }
    public float LocalAreaRadius { get; set; }
    public float Speed { get; set; }
    public float SteeringSpeed { get; set; }

    public void SimulateMovement(List<BoidController> other, float time)
    {
        //default vars
        var steering = Vector3.zero;

        //apply steering
        if (steering != Vector3.zero)
            transform.rotation = Quaternion.RotateTowards(transform.rotation, Quaternion.LookRotation(steering), SteeringSpeed * time);

        //move 
        transform.position += transform.TransformDirection(new Vector3(0, 0, Speed)) * time;
    }
}

Next create another Script called “SceneController” and attach it to a new empty GameObject in the scene. This is our interface to the Boids. It will spawn the Boids at startup and loop over them to update their movement. You can copy the following reference code for a quick setup.

public class SceneController : MonoBehaviour
{
    public BoidController boidPrefab;

    public int spawnBoids = 100;

    private List<BoidController> _boids;

    private void Start()
    {
        _boids = new List<BoidController>();

        for (int i = 0; i < spawnBoids; i++)
        {
            SpawnBoid(boidPrefab.gameObject, 0);
        }
    }

    private void Update()
    {
        foreach (BoidController boid in _boids)
        {
            boid.SimulateMovement(_boids, Time.deltaTime);
        }
    }

    private void SpawnBoid(GameObject prefab, int swarmIndex)
    {
        var boidInstance = Instantiate(prefab);
        boidInstance.transform.localPosition += new Vector3(Random.Range(-10, 10), Random.Range(-10, 10), Random.Range(-10, 10));
        _boids.Add(boidController);
    }
}

Most of this should be pretty obvious. We are defining some inspector variables: boidPrefab to hold our recently created Boid Prefab and spawnBoids so we can control how many Boids are spawned. The spawning will happen within 10 meters randomly around the start point.

Our Initial Simulation Values are:

  • Boid – Speed = 10f
  • Boid – SteeringSpeed = 100f
  • Boid – NoClumpingRadius = 5f
  • Boid – LocalAreaRadius = 10f

Let’s hit play and watch our base version of Boids. You can see they just move mindlessly into their respective forward direction.

Note: I have added color and a background to make it easier to distinguish Boids and Scenery. They are also teleported to the other side of the simulation area, when they pass the edge. You can find the whole code in the example project at the end.

The Logic

Now that our scene has been prepared we can start to implement the actual Boid concept. So back to some theory: Boids obay 3 core rules when determining their next movement step.

  • separation: steer to avoid crowding local neighbours
  • alignment: steer towards the average heading of local neighbours
  • cohesion: steer to move towards the average position (center of mass) of local neighbours

There are derived concepts that include more advanced features like inter-boid-communication, leadership, etc., but we will stick with the core rules for now.

As you can see the rules are based on locality, so we need to define what our local area encompasses. This value directly influences the simulation, so we want it to be changeable and the same for all of our Boids. It also shouldn’t be too big, as the main purpose of a local area is to have it smaller than the whole area available. That’s what our LocalAreaRadius and NoClumpingRadius in BoidController are for. I have already split them into two distinct values, because sooner than later you will want to give distinct values for these radii. This way you can control swarm size and local avoidance separately. A bigger LocalAreaRadius will increase the average swarm size, because each Boid has a bigger range of influence. On the other hand the NoClumpingRadius is directly linked to the density of swarms. The smaller the value the denser swarms can be on average, because each Boid has a smaller range where it will steer away from neighbours.

Separation

First we will add the code to simulate separation. Open your BoidController and head to SimulateMovement. We just need to calculate the average position vector of all local neighbours and steer into the exact opposite.

//separation vars
Vector3 separationDirection = Vector3.zero;
int separationCount = 0;

foreach (BoidController boid in other)
{
    //skip self
    if (boid == this)
        continue;

    var distance = Vector3.Distance(boid.transform.position, this.transform.position);

    //identify local neighbour
    if (distance < NoClumpingRadius)
    {
        separationDirection += boid.transform.position - transform.position;
        separationCount++;
    }
}

//calculate average
if (separationCount > 0)
        separationDirection /= separationCount;

//flip and normalize
separationDirection = -separationDirection.normalized;

//apply to steering
steering = separationDirection;

Boids will now try to avoid each other and steer into opposing directions. The higher our NoClumpingRadius, the more our Boids will try to get space between each other. For now our swarm behaviour is pretty random.

Alignment

Next up we will implement the alignment rule. Again we will loop over all local neighbours and calculate an average of vectors. This time we are using their respective heading directions for each Boid.

//identify local neighbour
if (distance < LocalAreaRadius)
{
    alignmentDirection += boid.transform.forward;
    alignmentCount++;
}

We will also need to apply the value to our steering.

steering += alignmentDirection;

This will cause the Boids to steer in the same direction as their local neighbours, while also trying to avoid clumping. Now since our NoClumpingRadius and LocalAreaRadius have the exact same values, Boids will tend to just disperse in random directions. To better see what happens I have lowered the size of the simulation area. As you can see the movement begins chaotic, but because they can’t escape the area the second rules kicks in and smooths alignments between Boids.

If we lower the NoClumpingRadius to 5f, we can see how they avoid each other until they are out of clumping range and then align with their neighbours.

Cohesion

Time for the final rule: Cohesion. It’s essentially the same as what we did for separation, but within our whole local area and unflipped.

if (distance < LocalAreaRadius)
{
    cohesionDirection += boid.transform.position - transform.position;
    cohesionCount++;
}

This time we also need to make our cohesion direction relative to the Boids position.

cohesionDirection -= transform.position;

And again apply it to steering.

steering += cohesionDirection;

Boids will now try to clump again and create temporary swarms based on our simulation parameters.

Tuning

All tweaking from here one depends on the result you are trying to achieve. In our case we want the Boids to behave like a swarm of fish, because we don’t need to do much to achieve this effect. So first of all we need to set the local area values to a low value for NoClumpingRadius (like 2), because fish swim very closely, and a high value for LocalAreaRadius (like 10), because they tend to form bigger swarms.

You may also want some rules to weigh higher in your calculation. There are many ways to implement a weighted algorithm, but as a very simple example you can alter your steering calculation like this.

steering += separationDirection.normalized * 0.5f;
steering += alignmentDirection.normalized * 0.34f;
steering += cohesionDirection.normalized * 0.16f;

The result is that our rules are now weighted as follows

  • separation: 50%
  • alignment: 34 %
  • cohesion: 16 %

This way you can control how much a rule influences the steering result. But keep in mind that the weights should add up to 1.

You could also prioritize a rule totally over any other. This code snippet will overwrite any other rule, if the Boid is trying to follow the separation rule. Only when the Boid is not trying to steer away from other Boids, will he follow any of the other rules.

if (separationDirection != Vector.zero)
    steering = separationDirection.normalized

Now I have only changed the NoClumpingRadius for this video down to 2f. As you can see the Boids will clump up closer and resemble a swarm of fish pretty accurately.

Leadership

The last thing that we need to implement in order to achieve a showable level of complexity is local leadership. Right now our Boids are always bunching up in the swarm. So let’s introduce a new mechanic: The Boid closest to another Boids forward vector, will become this Boid’s leader and the Boid will steer into the direction of its leader. So whatever a Boid spots first, will influence it’s steering path. We just need to check all local neighbours for the one with the least angular offset to our current forward vector.

//identify local neighbour
if (distance < LocalAreaRadius)
{
    var angle = Vector3.Angle(boid.transform.position - transform.position, transform.forward);
    if (angle < leaderAngle && angle < 90f)
    {
        leaderBoid = boid;
        leaderAngle = angle;
    }
}

Then we just apply the value to our steering variable like we did before.

if (leaderBoid != null)
    steering += (leaderBoid.transform.position - transform.position).normalized * 0.5f;

Now the change is subtle, but it gives the swarm a more organic touch. From here you can further enhance the leadership algorithm to split of lager swarms.

Environmental Awareness

Currently our Boids have zero awareness of what’s around them. So let’s give them the ability to “see” and avoid obstacles. Luckily this is actually pretty simple. We will just add a Raycast for every Boid to determine if they are about to hit something and steer in the opposite direction. This is a very important rule so we will apply it last and overwrite any previous steering.

RaycastHit hitInfo;
if (Physics.Raycast(transform.position, transform.forward, out hitInfo, LocalAreaRadius, LayerMask.GetMask("Default")))
    steering = -(hitInfo.point - transform.position).normalized;

I would advise you to use a dedicated layer for the Raycast (not “Default”) to keep the amount of queried objects as low as possible. Also keep in mind that any collider on the Boid will mess with the obstacle avoidance calculation, if the LayerMask contains this Layer.

Our Boids will now try to avoid any obstacles that we put into the scene, as long as these objects have a collider on them.

Points of Interest

The same way that we have just avoided some obstacles, we can also steer towards points of interest. For example, let’s try to bunch up our swarm around the coordinate center of (0, 0, 0). We just need to add yet another calculation to our movement. This time we will apply this after the core rules, but before the obstacle avoidance in a 1:1 ratio to the core rules. So obstacle avoidance is still the most important rule, and swarms will always move towards the point of interest.

var targetPoint = Vector3.zero;
steering += (targetPoint - transform.position).normalized;

As with any of the other rules you can alter the behaviour by changing the rule weight in relation to the other rules. You can also limit the attraction force of such points by only applying it in a specific radius. Like our other local rules. For example with the following version, the Boids will only steer to the point of interest, if they have it in their local area.

var targetPoint = Vector3.zero;
if (Vector3.Distance(transform.position, targetPoint) < LocalAreaRadius)
{
    steering += (targetPoint - transform.position).normalized;
}

Swarms

Our final rule will be to distinguish between different swarms. Each Boid will receive a new integer property called “SwarmIndex”.

public int SwarmIndex { get; set; }

Now with this we can change our basic three rules to respect the SwarmIndex of other Boids. A Boid will always try to steer away from other Boids, but only steer towards the heading and center of local neighbours that belong to it’s own swarm. We just need to change a single line in our SimulateMovement method:

//identify local neighbour
if (distance < LocalAreaRadius && boid.SwarmIndex == this.SwarmIndex)

Additionally we need to give a random SwarmIndex to our spawned Boids so we can see our new rule in action. Let’s create 3 random swarms for now.

boidController.SwarmIndex = Random.Range(0, 2);

We will also randomize the starting rotation of our Boids to give the simulation a more random start.

boidInstance.transform.localRotation = Quaternion.Euler(Random.Range(0, 360), Random.Range(0, 360), Random.Range(0, 360));

We now have a pretty organic simulation of multiple swarms.

Other optimizations

A lot of other rules can be implemented from here, like Fear, Communication, Splitting, Curiosity, etc. You can also add acceleration and deceleration for your Boids by applying a Vector3.SmoothDamp to the position delta. It’s really up to you and what the result should look like. Play around with the values a little to get a feel for what they actually alter in the swarms behaviour and how they work together. Implementing advanced rules shouldn’t be too hard.

Besides optimizing the swarm behaviour there is also the point of performance. Now keep in mind that tutorials are usually not optimized for that and so is this one. For example a Compute Shader or Job implementation with the BurstCompiler could yield way better performance for huge swarms. The Raycast uses the “Default” LayerMask for compatibility reasons, which should be changed to lower the amount of objects checked in each Raycast. And so on.

Example Project

The example project contains a scene with the things we did in this tutorial, as well as an example scene with a more complex fish model and render setup. A preview video is displayed below. You can download the full source code here:

https://github.com/RealDawnStudio/unity-tutorial-boids

It’s a zipped .unitypackage which you can import into any existing (or new) project. Feel free to use it in any of your projects. Except for the models, they are from a free site.

Closing Words

Now this is far from a perfect simulation obviously. But it’s a good starting point to get familiar with the concept. From here it should be easy to enhance it further. A few simple effects can do very much, as you can see.

Anyway, thanks for reading and have fun with your swarm!

Need help?

Join our Discord and ask one of our awesome members for help.

Spread the love