Flocking Behaviour, a Unity3D AI experiment

Flocking is usually referred to as the behaviour that animals follow when travelling in groups. In the natural world, flocking can be easily seen in birds that fly together, but also in fishes, insects and so on. I did a small AI project some time ago, and I thought I would share it with everyone.

Flocking behaviour can be modelled using three rules:

  • Alignment: As the name suggests, alignment is the rule by which an agent of the flock tries to align with the rest, essentially steering towards the average forward direction of travel of its neighboring agents.
  • Cohesion: Cohesion is the rule by which an agent tries to move towards the center of mass of a flock.
  • Separation: Separation works very similarly to cohesion, but it is the short distance repulsion between agents.

Using a few principles and ideas, it is possible to reproduce this behaviour in computers which can be used in games as well. This article will focus on the implementation of the Flocking Behaviour in Unity3D engine. I have seen many demonstrations and tutorials of the flocking behaviour in 2D on the web, but I couldn’t find a tutorial for how to implement this in Unity. Also, I wanted the demo to be in 3D, so even if the difference between 2D and 3D are minimal, this gives me the excuse to write an article about it.

I decided to go for a fish tank theme, where all my agents are fishes instead of birds. Because why not.

Entity.cs

First off, we will need an Entity class to hold data about the state of an agent inside the flock. The agent will have a velocity Vector3 that will be updated each frame so as to align and move according to the flocking behaviour.

public class Entity : MonoBehaviour
{
    private Vector3 mVelocity = new Vector3();
}

The Entity class really only needs to hold its velocity because position and orientation are contained in the transform component of the MonoBehaviour class.

On its update function, the flocking behaviour adds to the current agent’s velocity so as to get a smooth movement each frame.

void Update()
{
    mVelocity += FlockingBehaviour();
    mVelocity = Vector3.ClampMagnitude( mVelocity, mMaxVelocity );
    transform.position += mVelocity * Time.deltaTime;
    transform.forward = mVelocity.normalized;
    Reposition();
}

It also sets its forward vector with the one of the velocity vector.

Applying Flocking behaviour rules

When calculating Alignment, every agent of the flock will check the direction of all the other agents. This means that each agent will check all the other agents and calculate the average direction of the flock. To get this direction you can simply access the forward vector from the transform of the game object.

private Vector3 Alignment()
{
    List<Entity> theFlock = App.instance.theFlock;

    Vector3 alignmentVector = new Vector3();

    int count = 0;

    for( int index = 0; index < theFlock.Count; index++ )
    {
        if( mID != theFlock[ index ].ID )
        {
            float distance = ( transform.position - theFlock[ index ].transform.position ).sqrMagnitude; 

            if( distance > 0 && distance < mRadiusSquaredDistance )
            {
                alignmentVector += theFlock[ index ].transform.forward;
                count++;
            }
        }
    }

    if( count == 0 )
    {
         return Vector3.zero;
    }

    // forward step
    alignmentVector /= count;

    return alignmentVector;
}

Cohesion is none other than the vector pointing towards the center of mass of the flock. In order to calculate cohesion, each agent needs to calculate the average position of all the other agents. Then, it needs to calculate the direction towards the center of the flock from its current location. Since the step is very similar to the previous one, I will highlight the differences only.

for( int index = 0; index < theFlock.Count; index++ )
{
    if( mID != theFlock[ index ].ID )
    {
        float distance = ( transform.position - theFlock[ index ].transform.position ).sqrMagnitude; 

        if( distance > 0 && distance < mRadiusSquaredDistance )
        {
            cohesionVector += theFlock[ index ].transform.position;

            count++;
        }
    }
}

if( count == 0 )
{
    return Vector3.zero;
}

// cohesion step
cohesionVector /= count;
cohesionVector = ( cohesionVector - transform.position );

Separation is the last vector that needs to be calculated. Separation is expressed like the average distance from the current agent to all the other agents. Since the average distance vector obtained this way will point towards the other agents, we also need to invert it to get the separation vector.

for( int index = 0; index < theFlock.Count; index++ )
{
    // [excluding code...]
    separateVector += theFlock[ index ].transform.position - transform.position;
    // [excluding code...]
}

// [excluding code...]

// revert vector
// separation step
separateVector /= count;
separateVector *= -1;

Putting it all together

Instead of running the three loops separately for each rule, I decided to combine all the calculations into the same loop. Once all the three vectors are calculated, the easiest way to combine them is to simply sum them up. This will result in the velocity vector that is returned by the FlockingBehaviour method, as discussed previously with the Update method.

I also decided to add weights to each single vector. This makes it possible to change the flocking behaviour at runtime and experiment with how these three rules work together.

private Vector3 FlockingBehaviour()
{
    List&amp;lt;Entity&amp;gt; theFlock = App.instance.theFlock;

    Vector3 cohesionVector = new Vector3();
    Vector3 separateVector = new Vector3();
    Vector3 forwardVector = new Vector3();

    int count = 0;

    for( int index = 0; index &amp;lt; theFlock.Count; index++ )
    {
        if( mID != theFlock[ index ].ID )
        {
            float distance = ( transform.position - theFlock[ index ].transform.position ).sqrMagnitude;

            if( distance &amp;gt; 0 &amp;amp;&amp;amp; distance &amp;lt; mRadiusSquaredDistance )
            {
                cohesionVector += theFlock[ index ].transform.position;
                separateVector += theFlock[ index ].transform.position - transform.position;
                forwardVector += theFlock[ index ].transform.forward;

                count++;
            }
        }
    }

    if( count == 0 )
    {
        return Vector3.zero;
    }

    // revert vector
    // separation step
    separateVector /= count;
    separateVector *= -1;

    // forward step
    forwardVector /= count;

    // cohesione step
    cohesionVector /= count;
    cohesionVector = ( cohesionVector - transform.position );

    Vector3 flockingVector = ( ( separateVector.normalized * App.instance.separationWeight ) +
                             ( cohesionVector.normalized * App.instance.cohesionWeight ) +
                             ( forwardVector.normalized * App.instance.alignmentWeight ) );

    return flockingVector;
}

If you want to check out the whole project, you can find it on my Github.

Further implementations

Since every agent in the flock has to check against every other one each frame, this can be quite expensive when dealing with large number of agents of a flock. It would be a cool exercise to try optimizing the algorithm so that this is avoided. One way to go about it could be to subdivide the space into smaller spaces, for example using a grid approach. Each agent will register to the corresponding cell and then check their behavior only against the agents currently in its own cell, and the cells adjacent to it.

If you had a close look at the Update() method, you have certainly seen that there is a Reposition() method call in it. The reposition method force the fish to stay inside the boundaries of the tank. One thing that I would like to improve is the steering pattern of the method. It would be nice to have a smooth curve instead of the sudden change of direction that is currently implemented, so maybe adding an avoiding behavior of some sort would be great to improve the AI.

What about adding new rules to the flocking behaviour itself? Maybe adding Fear to the Entity state? This could be used if the flock has to run away from a Shark. Or maybe the flock is following some object of sort, like when lions follow the leader of their pack, for instance. That could be fun!

Flocking behaviour has a simple enough implementation but carries fascinating results that imitate natural behaviour. Use it in games when you are dealing with large groups of agents can be the way to go! Or maybe you have a fish tank where your Solid Snake-like character is thrown in by the baddie during a boss fight.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.