Proximity detection with triggers
Introduction
Imagine the following simple scenario: you have a creature in your game. He’s supposed to stand idle (perhaps grazing) until another character approaches within a certain range, and once that happens he should react to it. How do you implement this?
There are several options. One that is commonly used is to obtain a list of the relevant objects (for instance, the player character, or a list of other objects of the same type) and on the Update method, iterate through the list to find if the distance between them is shorter than some preset amount. This is the approach followed by the enemy script on Unity’s 3D platform tutorial, who keep checking for the player’s position.
This approach is good for a start, but has several disadvantages if you will be dealing with a large number of objects:
- Every object needs to be omniscient, and have a handle on every other object whose position they’ll need to check.
- Adding new objects on the fly (perhaps a base has spawned more soldiers) means that all references on each object will need to be updated.
- It doesn’t scale very well. If you have a group of n objects, all of which need to check each other positions to react, you’ll perform n*(n-1) calculations every frame. For a meager 100 characters, that would be 9,900 tests. At 30 frames per second, we’re talking about 279,000 possibly unnecessary distance checks per second. Those calculations might be fast, but why do them at all?
An intermediate approach might be to keep the references instead in a global controller, which knows about all the available objects. This would eliminate the need for updating every object’s references, and would reduce proximity checks to only SUM(0..n-1) for n objects, but it’s still a lot more work than necessary.
Enter Triggers
We’re going to explore an alternate approach. We’ll build a boid controller. Boids are a concept created by Craig Reynolds – without going into too much detail (that’s what web is there for) it’s meant to simulate the flocking behavior of birds, but has been successfully used for schools of fish, gazelle stampedes and many other scenarios.
The simplest boids rules are:
- Keep your distance from other objects
- Fly towards the centre of mass of neighboring boids
- Match velocity with nearby boids
With only these three rules, we can get a complex, very natural flocking behavior easily. No particular boid is designated as point, or told by the system you’re supposed to fit second from the left – the behavior just emerges.
Several boid implementations available online (such as this pseudocode) have a central controller which looks at all the other boid positions when altering a single boid behavior. What we will do implement this using a boid proximity detection via a collider trigger – no boid will have a view of the whole field, only of those few who are within range.
First we select a model – our humble champiro will do.

Add a rigidbody, and turn off gravity.

Now we go about implementing our rules. First we’ll need to know when a boid comes within visible range of one another, for the purposes of rules 2 and 3 above. Add a Sphere Collider to the boid, and set it to trigger. The sphere’s radius will be the area in which the boids detect each other.

We’ll also need to have them avoid crashing into each other. Most implementations do this via position altering, but we can do better: let’s add a box collider to our object.

At some point our objects will be flying around happily, and when they cross, the boid will receive an OnTriggerEnter event, along with an OnTriggerExit when we leave the collider’s area. This will happen regardless of if we cross with another visibility collider or the avoidance collider, so we need to distinguish between them.
A simple hack is to create two different physics materials, named Flock and Overcrowd, and assign Flock to the sphere collider and Overcrowd to the box collider. Once you’ve done that, create a BoidController script – I’ll use C# for this example.
using UnityEngine;
using System.Collections;
public class BoidController : MonoBehaviour {
public int defaultSpeed = 5;
public float maxSpeed = 100;
public Vector3 heading = new Vector3();
private float speed = 0.0f;
private Hashtable flockingBoids = new Hashtable();
// Use this for initialization
void Start () {
heading = Random.insideUnitSphere;
}
void OnTriggerEnter(Collider collision)
{
if (collision.material.name == “Flock (Instance)”)
flockingBoids[collision.transform.GetInstanceID()] = collision.transform;
}
void OnTriggerExit(Collider collision)
{
if (collision.material.name == “Flock (Instance)”)
flockingBoids.Remove(collision.transform.GetInstanceID());
}
}
This declares a few parameters, like the boids’ default speed and maximum speed. It also creates a hash table where we will store the boids that this boid can currently see.
The Start method should be pretty obvious – it simply starts each boid off in a random heading. We then detect other nearby boids when we receive a TriggerEnter or TriggerExit event. The handled for OnTriggerEnter adds a boid to the list of visible ones if we’ve been triggered by their Flocking collider, and OnTriggerExit removes it once they’re gone. Straightforward enough, isn’t it?
Now, you’ll notice that we’re not checking for the Overcrowd material. On my first implementation I actually handled that rule manually, but there is no need – we can simply let PhysX do the work for us. We do need to make sure that the collider we’re leaving when OnTriggerExit gets called is the Flock collider, so that we don’t remove a boid from our visibility area when the flocking trigger moves away from the overcrowding collider.
On to the position logic. Having added the boids we’re supposed to be flocking with to the collection above, we’ll now return a vector that will take us closer to their position.
Vector3 FlyTowardsNeighbors()
{
Vector3 avgPosition = new Vector3();
bool flocking = flockingBoids.Count > 0;
if (flocking)
{
foreach (DictionaryEntry de in flockingBoids)
{
Transform t = de.Value as Transform;
avgPosition += t.position;
}
avgPosition /= flockingBoids.Count;
}
Vector3 newVector = (avgPosition – transform.position) / 100;
return newVector;
}
But that’s not enough – we want to have the boid match speed with them as well:
Vector3 MatchSpeed()
{
Vector3 newSpeed = new Vector3();
bool flocking = flockingBoids.Count > 0;
if (flocking)
{
foreach (DictionaryEntry de in flockingBoids)
{
Transform t = de.Value as Transform;
BoidController c = (BoidController) t.GetComponent(“BoidController”);
newSpeed += c.heading;
}
newSpeed /= flockingBoids.Count ;
newSpeed = (newSpeed – this.heading) / 8;
}
return newSpeed;
}
On my tests the boid code tended to pick up speed with ease, so we’ll clamp it it within a preset parameter.
void ClipSpeed()
{
heading.x = Mathf.Clamp(heading.x, -maxSpeed, maxSpeed);
heading.y = Mathf.Clamp(heading.y, -maxSpeed, maxSpeed);
heading.z = Mathf.Clamp(heading.z, -maxSpeed, maxSpeed);
}
Finally it’s time to put it all together. On the Update method, we’ll evaluate both vectors, then alter the boid’s heading based on their results:
void Update () {
// If we just started, move!
if (speed == 0.0f)
{
speed = defaultSpeed;
}
Vector3 v1 = FlyTowardsNeighbors();
Vector3 v3 = MatchSpeed();
this.heading += v1 + v3;
ClipSpeed();
Quaternion target = Quaternion.Euler(heading);
transform.rotation = Quaternion.Slerp(transform.rotation, target, 1);
transform.Translate(heading * Time.deltaTime * speed);
}
With just that code, not only we have avoided the need for keeping and evaluating the position of all boids, but we have implemented things so that boid visibility is handled in the more natural way possible – when they actually enter each other’s area of perception. Add a group of objects to the scene, press play, and watch them flock!
If you throw in a couple obstacles, such as capsules, you’ll see them hit the collider and then change direction – flocks are likely to reform once they’ve passed the collider.
As you can see, the code we have ended with does not need to know how many other boids there are in the scene – sometimes it doesn’t even see other boids it’s flocking with.

On the image above you can see a single boid, with its visibility and overcrowding colliders highlighted. As you can see it’s only within visibility range of two flockmates, but the flock as a whole will have a lot of cohesion.

Conclusion
As we’ve seen, trigger colliders are an excellent way to detect actions within a certain radius of your characters, and provide a good alternative to global lists of objects to check for.
You can get a complete sample project from here. You can also view a web player version, which will reset the scene every 30 seconds (at which point many boids will have moved off-screen). Enjoy.










Hey this is excellent thank you for this tutorial and the explanations.
I’ll try to use this in my spaceship battle tactical game !