📜 ⬆️ ⬇️

Boid's, Birds and Unity3D



Second part: We optimize Boid's on Unity

Have you ever thought about why birds flying in large flocks never collide and collapse into a huge feather ball? Hmm, if you think about it, that would be awesome. In any case, one day in 1986, a man named Craig Reynolds was found who decided to create a simple model of bird behavior in flocks and called it Boids . In the model, each boat has three basic rules: Separation, Alignment and Cohesion. The first is to avoid collisions with neighbors, the second makes you fly about in the same direction as the neighbors, and the third says not to fly alone and keep the group. These simple rules allow you to create believable flocks of birds, fish and other living creatures, which is used in the film and gaming industry.
')
In the article I will tell you how to implement this model in practice. For development, I use Unity and C #, but most things are true for other engines and languages. In this tutorial, I do not chew the basics of working with Unity, it is assumed that you know the effect of the Ctrl + Shift + N combination on the stage, know how to work with the inspector, duplicate and move objects. If not, I advise you to start with this article . Or you can just look at the pictures.

Basic preparations




Let's create a new project in Unity and immediately build several daddies for the future: Materials, Prefabs, Scenes, Scripts.
We throw on the stage Directional Light and one sphere called Boid. We turn the sphere into a prefab. At the same time we will immediately save the scene, so that later not to think about it. Now we will start scripting.

For the model, we need to calculate three parameters: Separation, Alignment and Cohesion. Let's start with the last, it's the easiest. Let me remind you, this is a vector directed towards the center of the neighboring boids. To find it, you need to add the coordinates of the boids and divide the amount by their number. How does a Boid find out that he has neighbors? For this useful Physics.OverlapSphere . This function will return to us all colliders in a given cohesionRadius, including our Boid, if it falls into the sphere.
boids = Physics.OverlapSphere(transform.position, cohesionRadius); 

We zero out the variable, add, divide, and then draw a decorated line from the transformer to the center using the super- useful Debug.DrawLine and Color.magenta . Debug.DrawLine on the input accepts the coordinates of the beginning and end of the line, as well as an optional parameter for the color of the line. The results of the performance of all debugging functions are visible only during development, they just do not fall into the build.
 Debug.DrawLine(transform.position, cohesion, Color.magenta); 

Boid.cs center
 using UnityEngine; public class Boid : MonoBehaviour { private Vector3 cohesion; private float cohesionRadius = 10; private Collider[] boids; void Update() { cohesion = Vector3.zero; boids = Physics.OverlapSphere(transform.position, cohesionRadius); foreach (var boid in boids) { cohesion += boid.transform.position; } cohesion = cohesion / boids.Length; Debug.DrawLine(transform.position, cohesion, Color.magenta); } } 

We throw the script on the prefab, copy the boyd a couple of times and click Play. Don't forget to turn on the Gizmos display otherwise you won't see the line.



We collect boida in a bunch


So it seems to work. Now we need to turn the resulting point into motion. It’s not good to keep everything in one heap, so we’ll move the previous code into a separate function. The function will be run on a timer using InvokeRepeating . The first argument is the name of the function, the second is the start time, the third is the repetition interval. This feature is very useful for delaying the launch of various scripts.
 InvokeRepeating("CalculateVelocity", 0, 1); 

To calculate the vector, we use school mathematics and subtract the coordinates of the boida from the coordinates of the center. Add a public variable (I’ll say why) later to the script, reset it at the beginning of the function, and add a new cohesion vector to it at the end. In Update, we attach the result to the coordinates of the transform, taking into account the past time. Time.deltaTime is needed to ensure that the movement does not depend on the FPS and goes with the same speed on all processors.
 transform.position += velocity * Time.deltaTime; 

In addition, since our center has become a vector, then we will change our Debug.DrawLine to another equally fantastic Debug.DrawRay . No difference, just the second argument should be in relative coordinates, just like ours.

Boid.cs cohesion
 using UnityEngine; public class Boid : MonoBehaviour { public Vector3 velocity; private float cohesionRadius = 10; private Collider[] boids; private Vector3 cohesion; private void Start() { InvokeRepeating("CalculateVelocity", 0, 1); } void CalculateVelocity() { velocity = Vector3.zero; cohesion = Vector3.zero; boids = Physics.OverlapSphere(transform.position, cohesionRadius); foreach (var boid in boids) { cohesion += boid.transform.position; } cohesion = cohesion / boids.Length; cohesion = cohesion - transform.position; velocity += cohesion; } void Update() { transform.position += velocity * Time.deltaTime; Debug.DrawRay(transform.position, cohesion, Color.magenta); } } 



We divide the boids


The separation calculation is a bit more complicated. It is necessary to calculate the most useful direction of exit from the pile of neighbors. To do this, we can find the weighted sum of the vectors from each neighbor. We divide a vector from a neighbor by the distance to it, which is obtained using Vector3.magnitude . In the resulting amount, the nearest neighbors will have the greatest weight.
 separation += (transform.position - boid.transform.position) / (transform.position - boid.transform.position).magnitude; 

It makes sense to limit the number of considered neighbors to a certain distance, for this we add one variable for the counter and one for the separation radius.
 if ((transform.position - boid.transform.position).magnitude < separationDistance) 

In addition, we do not need to get a zero vector in the amount due to the collider of the Boida itself. Do not forget that Physics.OverlapSphere covers all colliders, including the Boid collider. Therefore, we slightly change the condition.
 if (boid != collider && (transform.position - boid.transform.position).magnitude < separationDistance) 

Boid.cs separation
 using UnityEngine; public class Boid : MonoBehaviour { public Vector3 velocity; private float cohesionRadius = 10; private float separationDistance = 5; private Collider[] boids; private Vector3 cohesion; private Vector3 separation; private int separationCount; private void Start() { InvokeRepeating("CalculateVelocity", 0, 1); } void CalculateVelocity() { velocity = Vector3.zero; cohesion = Vector3.zero; separation = Vector3.zero; separationCount = 0; boids = Physics.OverlapSphere(transform.position, cohesionRadius); foreach (var boid in boids) { cohesion += boid.transform.position; if (boid != collider && (transform.position - boid.transform.position).magnitude < separationDistance) { separation += (transform.position - boid.transform.position) / (transform.position - boid.transform.position).magnitude; separationCount++; } } cohesion = cohesion / boids.Length; cohesion = cohesion - transform.position; if (separationCount > 0) { separation = separation / separationCount; } velocity += cohesion + separation; } void Update() { transform.position += velocity * Time.deltaTime; Debug.DrawRay(transform.position, separation, Color.green); Debug.DrawRay(transform.position, cohesion, Color.magenta); } } 



We organize boids


In order for boids not just thoughtlessly to form even piles, we need them to repeat the behavior of their neighbors. The alignment calculation is very simple, we summarize the public velocity variables (aha!) From each neighbor and divide by their number. Attached scripts can be accessed using GameObject.GetComponent . He can find not only scripts, but in general any components. Wonderful stuff.
 alignment += boid.GetComponent<Boid>().velocity; 

Boid.cs alignment
 using UnityEngine; public class Boid : MonoBehaviour { public Vector3 velocity; private float cohesionRadius = 10; private float separationDistance = 5; private Collider[] boids; private Vector3 cohesion; private Vector3 separation; private int separationCount; private Vector3 alignment; private void Start() { InvokeRepeating("CalculateVelocity", 0, 1); } void CalculateVelocity() { velocity = Vector3.zero; cohesion = Vector3.zero; separation = Vector3.zero; separationCount = 0; alignment = Vector3.zero; boids = Physics.OverlapSphere(transform.position, cohesionRadius); foreach (var boid in boids) { cohesion += boid.transform.position; alignment += boid.GetComponent<Boid>().velocity; if (boid != collider && (transform.position - boid.transform.position).magnitude < separationDistance) { separation += (transform.position - boid.transform.position) / (transform.position - boid.transform.position).magnitude; separationCount++; } } cohesion = cohesion / boids.Length; cohesion = cohesion - transform.position; if (separationCount > 0) { separation = separation / separationCount; } alignment = alignment / boids.Length; velocity += cohesion + separation + alignment; } void Update() { transform.position += velocity * Time.deltaTime; Debug.DrawRay(transform.position, separation, Color.green); Debug.DrawRay(transform.position, cohesion, Color.magenta); Debug.DrawRay(transform.position, alignment, Color.blue); } } 

We start and ... zero reaction, everything is the same. Add a two to the velocity calculation formula.
 velocity += cohesion + separation + alignment*2; 

And then…



We cut vectors


Vsuuum! Well, quite predictable. We increased the alignment vector, which increased the velocity vector, which increased the alignment vector, which ... Well, you understand. We need to make a maximum speed limit. Moreover, the restriction should be put on all components of the vector, otherwise in some situations the behavior of the boat becomes somewhat strange. You can try it yourself.

For clipping vectors in Unity, there is a Vector3.ClampMagnitude function. After adding each vector, we simply add the following construction:
 velocity = Vector3.ClampMagnitude(velocity, maxSpeed); 

Boid.cs clamp
 using UnityEngine; public class Boid : MonoBehaviour { public Vector3 velocity; private float cohesionRadius = 10; private float separationDistance = 5; private Collider[] boids; private Vector3 cohesion; private Vector3 separation; private int separationCount; private Vector3 alignment; private float maxSpeed = 15; private void Start() { InvokeRepeating("CalculateVelocity", 0, 1f); } void CalculateVelocity() { velocity = Vector3.zero; cohesion = Vector3.zero; separation = Vector3.zero; separationCount = 0; alignment = Vector3.zero; boids = Physics.OverlapSphere(transform.position, cohesionRadius); foreach (var boid in boids) { cohesion += boid.transform.position; alignment += boid.GetComponent<Boid>().velocity; if (boid != collider && (transform.position - boid.transform.position).magnitude < separationDistance) { separation += (transform.position - boid.transform.position) / (transform.position - boid.transform.position).magnitude; separationCount++; } } cohesion = cohesion / boids.Length; cohesion = cohesion - transform.position; cohesion = Vector3.ClampMagnitude(cohesion, maxSpeed); if (separationCount > 0) { separation = separation / separationCount; separation = Vector3.ClampMagnitude(separation, maxSpeed); } alignment = alignment / boids.Length; alignment = Vector3.ClampMagnitude(alignment, maxSpeed); velocity += cohesion + separation * 10 + alignment * 1.5f; velocity = Vector3.ClampMagnitude(velocity, maxSpeed); } void Update() { if (transform.position.magnitude > 25) { velocity += -transform.position.normalized; } transform.position += velocity * Time.deltaTime; Debug.DrawRay(transform.position, separation, Color.green); Debug.DrawRay(transform.position, cohesion, Color.magenta); Debug.DrawRay(transform.position, alignment, Color.blue); } } 

We check the work of pacified vectors.



Automating


It is not at all interesting to arrange boids by hand. For software placement there is an Instantiate function. At the entrance she needs to submit a link to the object to copy, the new coordinates of the object and its rotation. For the copied prefab, we make a separate public variable, which we will fill in the inspector. It is convenient to take random coordinates from Random.insideUnitSphere , simply multiply it by the radius of the required sphere. You can rotate our boids as much as you want, the result will be the same, so we use Quaternion.identity , which means no rotation.
 Instantiate(boidPrefab, Random.insideUnitSphere * 25, Quaternion.identity); 

In the cycle, repeat the action above and get any desired number of boids. We throw a new script on an empty object in the center of the scene and fill in the link to the prefab.

HeartOfTheSwarm.cs
 using UnityEngine; public class HeartOfTheSwarm : MonoBehaviour { public Transform boidPrefab; public int swarmCount = 100; void Start() { for (var i = 0; i < swarmCount; i++) { Instantiate(boidPrefab, Random.insideUnitSphere * 25, Quaternion.identity); } } } 

It is not very convenient to observe a rapidly moving flight of boids, it would be good to put them on a chain. To do this, add a small condition to Update:
 if (transform.position.magnitude > 25) { velocity += -transform.position.normalized; } 

With it, boids, whose coordinates are located outside the virtual sphere, will turn towards the center. Finally, let's play a bit with vector multipliers and other parameters, otherwise the desired effect will not work. See the final code under the spoiler below.
 velocity += cohesion + separation * 10 + alignment * 1.5f; 

Run, admire.

Boid.cs
 using UnityEngine; public class Boid : MonoBehaviour { public Vector3 velocity; private float cohesionRadius = 10; private float separationDistance = 5; private Collider[] boids; private Vector3 cohesion; private Vector3 separation; private int separationCount; private Vector3 alignment; private float maxSpeed = 15; private void Start() { InvokeRepeating("CalculateVelocity", 0, 0.1f); } void CalculateVelocity() { velocity = Vector3.zero; cohesion = Vector3.zero; separation = Vector3.zero; separationCount = 0; alignment = Vector3.zero; boids = Physics.OverlapSphere(transform.position, cohesionRadius); foreach (var boid in boids) { cohesion += boid.transform.position; alignment += boid.GetComponent<Boid>().velocity; if (boid != collider && (transform.position - boid.transform.position).magnitude < separationDistance) { separation += (transform.position - boid.transform.position) / (transform.position - boid.transform.position).magnitude; separationCount++; } } cohesion = cohesion / boids.Length; cohesion = cohesion - transform.position; cohesion = Vector3.ClampMagnitude(cohesion, maxSpeed); if (separationCount > 0) { separation = separation / separationCount; separation = Vector3.ClampMagnitude(separation, maxSpeed); } alignment = alignment / boids.Length; alignment = Vector3.ClampMagnitude(alignment, maxSpeed); velocity += cohesion + separation * 10 + alignment * 1.5f; velocity = Vector3.ClampMagnitude(velocity, maxSpeed); } void Update() { if (transform.position.magnitude > 25) { velocity += -transform.position.normalized; } transform.position += velocity * Time.deltaTime; Debug.DrawRay(transform.position, separation, Color.green); Debug.DrawRay(transform.position, cohesion, Color.magenta); Debug.DrawRay(transform.position, alignment, Color.blue); } } 



That's all, boids fly in their cage. However, there are so few of them! With the amount of more than a hundred, everything starts to slow down significantly. No wonder, because we have not done a single optimization. In the next part, I will discuss how to optimize our code so that it can hold 60 FPS on a much larger number of boids. In the meantime, you can offer your options in the comments.

Second part: We optimize Boid's on Unity

Note: The code for the links below is outdated; see the Procedural Toolkit for the latest version.

Sources on GitHub | Online version for owners of Unity Web Player

Mini bonus for those who are interested in how I did the animation.
Screenshot.cs
 using UnityEngine; public class Screenshot : MonoBehaviour { private int count; void Update() { if (Input.GetButtonDown("Jump")) { InvokeRepeating("Capture", 0.1f, 0.3f); } } void Capture() { Application.CaptureScreenshot(Application.dataPath + "/Screenshot" + count + ".png"); count++; } } 

Source: https://habr.com/ru/post/182382/


All Articles