In this section, we'll create our own scene with flocks of objects and implement the flocking behavior in C#. There are two main components in this example: the individual boid behavior and a main controller to maintain and lead the crowd.
Our scene hierarchy is shown in the following screenshot:
As you can see, we have several boid entities, UnityFlock
, under a controller named UnityFlockController
. The UnityFlock
entities are individual boid objects and they'll reference to their parent UnityFlockController
entity to use it as a leader. The UnityFlockController
entity will update the next destination point randomly once it reaches the current destination point.
The UnityFlock
prefab is a prefab with just a cube mesh and a UnityFlock
script. We can use any other mesh representation for this prefab to represent something more interesting, like birds.
Boid is a term, coined by Craig Reynold, that refers to a bird-like object. We'll use this term to describe each individual object in our flock. Now let's implement our boid behavior. You can find the following script in UnityFlock.cs
, and this is the behavior that controls each boid in our flock.
The code in the UnityFlock.cs
file is as follows:
using UnityEngine; using System.Collections; public class UnityFlock : MonoBehaviour { public float minSpeed = 20.0f; public float turnSpeed = 20.0f; public float randomFreq = 20.0f; public float randomForce = 20.0f; //alignment variables public float toOriginForce = 50.0f; public float toOriginRange = 100.0f; public float gravity = 2.0f; //seperation variables public float avoidanceRadius = 50.0f; public float avoidanceForce = 20.0f; //cohesion variables public float followVelocity = 4.0f; public float followRadius = 40.0f; //these variables control the movement of the boid private Transform origin; private Vector3 velocity; private Vector3 normalizedVelocity; private Vector3 randomPush; private Vector3 originPush; private Transform[] objects; private UnityFlock[] otherFlocks; private Transform transformComponent;
We declare the input values for our algorithm that can be set up and customized from the editor. First, we define the minimum movement speed, minSpeed
, and rotation speed, turnSpeed
, for our boid. The randomFreq
value is used to determine how many times we want to update the randomPush
value based on the randomForce
value. This force creates a randomly increased and decreased velocity and makes the flock movement look more realistic.
The toOriginRange
value specifies how spread out we want our flock to be. We also use toOriginForce
to keep the boids in range and maintain a distance with the flock's origin. Basically, these are the properties to deal with the alignment rule of our flocking algorithm. The avoidanceRadius
and avoidanceForce
properties are used to maintain a minimum distance between individual boids. These are the properties that apply the separation rule to our flock.
The followRadius
and followVelocity
values are used to keep a minimum distance with the leader or the origin of the flock. They are used to comply with the cohesion rule of the flocking algorithm.
The origin
object will be the parent object to control the whole group of flocking objects. Our boid needs to know about the other boids in the flock. So, we use the objects
and otherFlocks
properties to store the neighboring boids' information.
The following is the initialization method for our boid:
void Start () { randomFreq = 1.0f / randomFreq; //Assign the parent as origin origin = transform.parent; //Flock transform transformComponent = transform; //Temporary components Component[] tempFlocks= null; //Get all the unity flock components from the parent //transform in the group if (transform.parent) { tempFlocks = transform.parent.GetComponentsInChildren <UnityFlock>(); } //Assign and store all the flock objects in this group objects = new Transform[tempFlocks.Length]; otherFlocks = new UnityFlock[tempFlocks.Length]; for (int i = 0;i<tempFlocks.Length;i++) { objects[i] = tempFlocks[i].transform; otherFlocks[i] = (UnityFlock)tempFlocks[i]; } //Null Parent as the flock leader will be //UnityFlockController object transform.parent = null; //Calculate random push depends on the random frequency //provided StartCoroutine(UpdateRandom()); }
We set the parent of the object of our boid as origin
; it means that this will be the controller object to follow generally. Then, we grab all the other boids in the group and store them in our own variables for later references.
The StartCoroutine
method starts the UpdateRandom()
method as a co-routine:
IEnumerator UpdateRandom() { while (true) { randomPush = Random.insideUnitSphere * randomForce; yield return new WaitForSeconds(randomFreq + Random.Range(-randomFreq / 2.0f, randomFreq / 2.0f)); } }
The UpdateRandom()
method updates the randomPush
value throughout the game with an interval based on randomFreq
. The Random.insideUnitSphere
part returns a Vector3
object with random x, y, and z values within a sphere with a radius of the randomForce
value. Then, we wait for a certain random amount of time before resuming the while(true)
loop to update the randomPush
value again.
Now, here's our boid behavior's Update()
method that helps our boid entity comply with the three rules of the flocking algorithm:
void Update () { //Internal variables float speed = velocity.magnitude; Vector3 avgVelocity = Vector3.zero; Vector3 avgPosition = Vector3.zero; float count = 0; float f = 0.0f; float d = 0.0f; Vector3 myPosition = transformComponent.position; Vector3 forceV; Vector3 toAvg; Vector3 wantedVel; for (int i = 0;i<objects.Length;i++){ Transform transform= objects[i]; if (transform != transformComponent) { Vector3 otherPosition = transform.position; // Average position to calculate cohesion avgPosition += otherPosition; count++; //Directional vector from other flock to this flock forceV = myPosition - otherPosition; //Magnitude of that directional vector(Length) d= forceV.magnitude; //Add push value if the magnitude, the length of the //vector, is less than followRadius to the leader if (d < followRadius) { //calculate the velocity, the speed of the object, based //on the avoidance distance between flocks if the //current magnitude is less than the specified //avoidance radius if (d < avoidanceRadius) { f = 1.0f - (d / avoidanceRadius); if (d > 0) avgVelocity += (forceV / d) * f * avoidanceForce; } //just keep the current distance with the leader f = d / followRadius; UnityFlock tempOtherFlock = otherFlocks[i]; //we normalize the tempOtherFlock velocity vector to get //the direction of movement, then we set a new velocity avgVelocity += tempOtherFlock.normalizedVelocity * f * followVelocity; } } }
The preceding code implements the separation rule. First, we check the distance between the current boid and the other boids and update the velocity accordingly, as explained in the comments.
Next, we calculate the average velocity of the flock by dividing the current velocity with the number of boids in the flock:
if (count > 0) { //Calculate the average flock velocity(Alignment) avgVelocity /= count; //Calculate Center value of the flock(Cohesion) toAvg = (avgPosition / count) - myPosition; } else { toAvg = Vector3.zero; } //Directional Vector to the leader forceV = origin.position - myPosition; d = forceV.magnitude; f = d / toOriginRange; //Calculate the velocity of the flock to the leader if (d > 0) //if this void is not at the center of the flock originPush = (forceV / d) * f * toOriginForce; if (speed < minSpeed && speed > 0) { velocity = (velocity / speed) * minSpeed; } wantedVel = velocity; //Calculate final velocity wantedVel -= wantedVel * Time.deltaTime; wantedVel += randomPush * Time.deltaTime; wantedVel += originPush * Time.deltaTime; wantedVel += avgVelocity * Time.deltaTime; wantedVel += toAvg.normalized * gravity * Time.deltaTime; //Final Velocity to rotate the flock into velocity = Vector3.RotateTowards(velocity, wantedVel, turnSpeed * Time.deltaTime, 100.00f); transformComponent.rotation = Quaternion.LookRotation(velocity); //Move the flock based on the calculated velocity transformComponent.Translate(velocity * Time.deltaTime, Space.World); //normalise the velocity normalizedVelocity = velocity.normalized; } }
Finally, we add up all the factors such as randomPush
, originPush
, and avgVelocity
to calculate our final target velocity, wantedVel
. We also update our current velocity
to wantedVel
with linear interpolation using the Vector3.RotateTowards
method. Then, we move our boid based on the new velocity using the Translate()
method.
Next, we create a cube mesh and add this UnityFlock
script to it, and make it a prefab, as shown in the following screenshot:
Now it is time to create the controller class. This class updates its own position so that the other individual boid objects know where to go. This object is referenced in the origin
variable in the preceding UnityFlock
script.
The code in the UnityFlockController.cs
file is as follows:
using UnityEngine; using System.Collections; public class UnityFlockController : MonoBehaviour { public Vector3 offset; public Vector3 bound; public float speed = 100.0f; private Vector3 initialPosition; private Vector3 nextMovementPoint; // Use this for initialization void Start () { initialPosition = transform.position; CalculateNextMovementPoint(); } // Update is called once per frame void Update () { transform.Translate(Vector3.forward * speed * Time.deltaTime); transform.rotation = Quaternion.Slerp(transform.rotation, Quaternion.LookRotation(nextMovementPoint - transform.position), 1.0f * Time.deltaTime); if (Vector3.Distance(nextMovementPoint, transform.position) <= 10.0f) CalculateNextMovementPoint(); }
In our Update()
method, we check whether our controller object is near the target destination point. If it is, we update our nextMovementPoint
variable again with the CalculateNextMovementPoint()
method we just discussed:
void CalculateNextMovementPoint () { float posX = Random.Range(initialPosition.x - bound.x, initialPosition.x + bound.x); float posY = Random.Range(initialPosition.y - bound.y, initialPosition.y + bound.y); float posZ = Random.Range(initialPosition.z - bound.z, initialPosition.z + bound.z); nextMovementPoint = initialPosition + new Vector3(posX, posY, posZ); } }
The CalculateNextMovementPoint()
method finds the next random destination position in a range between the current position and the boundary vectors.
Putting it all together, as shown in the previous scene hierarchy screenshot, you should have flocks flying around somewhat realistically: