BOIDS

capture

Demonstration of swarm behavior game AI and emergence.

TECH: Unity 5 | C#

My implementation of Boids, the classic computer model for coordinated animal motion created in 1986 by Craig Reynolds. Through simple vector math performed by the “boids” visibly complex behavior resembling flocking emerges.

Separation Vector – vector directed to avoid crowding neighboring flockmates

Alignment Vector vector directed towards the average heading of neighboring flockmates

Cohesion Vector – vector directed towards the average position of neighboring flockmates

I followed the CoreFX C# Coding Style as my style guide for this project.

Autonomous versus Managed

My Boid class is capable of switching between different twists on boids implementations on the fly:

public enum BoidType
{
	Autonomous,
	Managed
};

This enum can scale up to support more advanced implementations, such as a flock leader system and so on.

Autonomous boids simply flock towards their neighbors of the same BoidType, meaning they are both running the autonomous implementation and therefore flockmates. All the same they avoid anything encroaching into their separation proximity regardless of if they are flockmates.

Managed boids flock towards their neighbors of the same BoidType, their flockmates, but are additionally provided a goal position by their BoidManager. The position is utilized in alignment vector calculations. They also avoid anything encroaching into their separation proximity, and as such can intelligently traverse obstacles in order to reach their goal position.

public class Boid : MonoBehaviour 
{
	// Reference to BoidsController object for Managed Boid objects
	[HideInInspector] public BoidsController parentBoidsController;
	public BoidType boidType;
	public float moveSpeed;			// Movement speed in editor units.
	public float rotationPercentage;	// % of move speed for rotation speed.
	public float separationProximity;	// Min distance from neighbough for corrective steering.
	public float neighbourDetectRadius;	// Boid objects within radius will comprise neigbour group

	private Vector3 _separation	= Vector3.zero; // Avoidance vector for separation.
	private Vector3 _alignment 	= Vector3.zero; // Alignment vector for positioning within group.
	private Vector3 _cohesion	= Vector3.zero; // Direction vector for _cohesion.
	private Vector3 _groupAverageHeading;
	private Vector3 _groupAveragePosition;		// Averaged position of all neighbours.
	private Vector3	_spawnLocation;			// The spawn location of this Boid.
	
	private int _groupSize 		= 0;		// Total Boids in neighbour group.
	private float _groupSpeed 	= 0f; 		// Total speed of neighbour group.
	private float _distanceToCurrentNeighbour;

The public variables can be tweaked in the editor to get a feel for the right balance of spacing between boids while the program is running. An excessive turning speed can ruin the illusion of natural movement, so rather than set the speed of rotation directly the rotationPercentage float is used to scale rotation speed to the movement speed of the boid.

        // Delegate used to update trajectory of this Boid.
	delegate void TrajectoryUpdate();
	private TrajectoryUpdate trajectoryUpdate;

	void Awake()
	{
		_spawnLocation = transform.position;
		SetTrajectoryUpdate();
	}

	void Update() 
	{
		trajectoryUpdate();
		// Translate along Z-axis in calculated trajectory
		transform.Translate(0, 0, Time.deltaTime * moveSpeed);
	}

	void SetTrajectoryUpdate()
	{
		switch (boidType)
		{
			case BoidType.Autonomous:
				trajectoryUpdate = AutonomousUpdateTrajectory;
				break;
			case BoidType.Managed:
				trajectoryUpdate = ManagedUpdateTrajectory;
				break;
			default:
				break;
		}
	}

The delegate TrajectoryUpdate is used here to improve the readability and performance of the update loop. SetTrajectoryUpdate quickly and concisely provides BoidType swapping functionality with far more elegance than placing a switch in the update loop to check the BoidType each frame.

Autonomous Boid Trajectory Calculation

        // Leaderless, no consideration for goal position.
	void AutonomousUpdateTrajectory()
	{
		_separation	= Vector3.zero;
		_alignment	= Vector3.zero;

		// Obtain neighbours.
		Collider[] neigbourColliders = Physics.OverlapSphere(transform.position, neighbourDetectRadius);

		List neighboughs = new List();
		for (int i = 0; i < neigbourColliders.Length; i++)
		{
			neighboughs.Add(neigbourColliders[i].gameObject);
		}
		neighboughs.Remove(gameObject); // Remove self from List before iteration.

		foreach (GameObject gameObj in neighboughs)
		{
			_distanceToCurrentNeighbour = Vector3.Distance(gameObj.transform.position, transform.position);
			if (_distanceToCurrentNeighbour <= neighbourDetectRadius)                         {                                                               DetectedGameObjectResponse(gameObj);                         }    		}                 if (_groupSize > 0)
		{
			_alignment = _alignment / _groupSize; // Calculate center.
			_cohesion = (_alignment + _separation) - transform.position;
			if (_cohesion != Vector3.zero)
			{
				transform.rotation = Quaternion.Slerp(transform.rotation,
									Quaternion.LookRotation(_cohesion),
									rotationPercentage * moveSpeed * Time.deltaTime);
			}
		}
	}

Physics.OverLapSphere ensures the boid collects even non-boid scene GameObjects with colliders for the sake of separation calculations, preventing illusion-breaking behaviors like swimming directly into walls.

Managed Boid Trajectory Calculation

        // Managed by BoidsManager. Homes in on goal position while accounting for neighbours.
	void ManagedUpdateTrajectory()
	{
		_separation	= Vector3.zero;
		_alignment	= Vector3.zero;

		parentBoidsController.allBoidGameObjects.Remove(gameObject); // Remove self from List before iteration.
		foreach (GameObject gameObj in parentBoidsController.allBoidGameObjects)
		{
			_distanceToCurrentNeighbour = Vector3.Distance(gameObj.transform.position, transform.position);
			if (_distanceToCurrentNeighbour <= neighbourDetectRadius)                         {                                 DetectedGameObjectResponse(gameObj);                         }    		}                 parentBoidsController.allBoidGameObjects.Add(gameObject); // Add this back to List                 if (_groupSize > 0)
		{
			// Calculate center and add Vector to Goal.
			_alignment = (_alignment / _groupSize) + (parentBoidsController.GetGoalPosition() - transform.position);
			_cohesion = (_alignment + _separation) - transform.position;
			if (_cohesion != Vector3.zero)
			{
				transform.rotation = Quaternion.Slerp(transform.rotation,
							Quaternion.LookRotation(_cohesion),
							rotationPercentage * moveSpeed * Time.deltaTime);
			}
		}
	}

The DetectedGameObjectResponse function encompasses universal boid behaviours and significantly improved the style and readability of the respective trajectory updating functions.

	// Response to neighbouring GameObject
	void DetectedGameObjectResponse(GameObject gameObj)
	{
		Boid otherBoid = gameObj.GetComponentInParent();
		if (otherBoid && otherBoid.boidType == boidType)
		{
			_alignment += gameObj.transform.position; // Add neighbour positions for averaging.
			_groupSpeed += otherBoid.moveSpeed;
			_groupSize++;
		}
		// Account for _separation correction if experiencing proximity intrusion.
		if (_distanceToCurrentNeighbour < separationProximity)
		{
			_separation += transform.position - gameObj.transform.position;
		}
	}
}