Getting Tricky with Triggers

Unity triggers are insanely useful.  If you aren’t using physics triggers–even if your game is seemingly non-physical–you’re missing out on some of the most powerful functionality in Unity.  Let’s take a look:

What’s a Trigger?

A trigger is a Collider that fires events as other Colliders enter and leave its collision volume, but without physically interacting with your scene. They are physically “invisible”.  If a Rigidbody “hits” a trigger, it passes right through, but the trigger produces OnTriggerEnter calls, when the object enters, OnTriggerStay calls, for as long as the object is inside the trigger, and OnTriggerExit calls, for when the object leaves the trigger.

Take a look at the Unity collider documentation for a more verbose explanation, as well as the collision chart for when collision events are called.

Where to Use Triggers?

We use triggers as:

Static Proximity Triggers

The most obvious trigger example is to use them as, well, actual proximity triggers. Some piece of game logic is run when the player, or another object, reaches a point in space.  You could place a trigger in front of a door which causes the door to open as the player approaches.  You can easily test the entered Collider for some layer, tag, or Component to see if the trigger should execute.  This is as sample as:

function OnTriggerEnter(other:Collider)
{
   // only process if we’re the player
   if(!other.CompareTag(“Player”))
      return;
   
   // does the player have the blue key?  if not, boo
   if(!Player.HasKey(“blue”))
      return;
      
   // actually open the door!
}

Radius Triggers

The first trick with triggers is to realize they can move. You can set a trigger as a child of an object–even another physical object like a Rigidbody–and then use the trigger to take action as other objects approach or exit a radius around your object of interest.

Take the use case of spawn points, for instance; your goal is to trigger an enemy spawn as a spawn point nears your player. You could place a script on your player that iterates all possible spawn points and spawns something if it’s near enough:

function FixedUpdate()
{
for(var spawn:SpawnPoint in allSpawns)
   if(Vector3.Distance(transform.position, spawn.transform.position < spawnRadius)
      spawn.DoSpawn();
}

The #1 reason for using triggers, over this “manual” method, is that triggers will be faster. Much faster! There are ways to make this example better–check the sqrMagnitude of the distance to avoid a square root, cache your Transform, etc–but the crux of the problem remains: You have to run this code every Update, FixedUpdate, or at least with enough frequency to respond interactively.

However, the physics engine is already checking for trigger collisions. It’s doing the work for you! Let it do its super-fast optimized thing; believe me, you can’t compete with PhysX on an optimization level. Instead of polling for spawn points in our radius ourselves, let the physics engine send events only when something enters our player radius trigger. This becomes:

function OnTriggerEnter(other:Collider)
{
   var spawn:SpawnPoint = other.GetComponent(SpawnPoint);
   
   if(spawn)
      spawn.DoSpawn();
}

Create a gigantic trigger as a child of your player object, place this script on it, and voila! To prevent collision with your physical gameplay objects, your spawn colliders should also be triggers (yes, triggers send OnTrigger* messages when other triggers enter and exit).

Warning: Trigger colliders do respond to raycasts. You should make sure your triggers are set to the Ignore Raycasts layer if your game logic uses raycasts (or are otherwise ignored by your raycast LayerMasks).

What’s In This Space?

Another great use of triggers is to the answer a question for your game logic: What’s in this space? Perhaps you want your spawn points to fire only if they’re empty, or you want to see if two objects are near enough to count for something.

Here’s a concerete example: One of our games, Crane Wars, is a stacking game. In order to determine if different building pieces are stacked on top of one another, and should count as one building, we use triggers:

The slim box outlines on the top of the building pieces are the triggers. White indicates that nothing is in the trigger, and green–the bottom two floors of the stack–indicate that another building piece is in the trigger and is considered the “upstairs” piece in the sequence. The light blue box is a spawn point; it will spawn another piece as soon as it is empty.

In order to facilitate this game logic, we have generic scripts that track objects as they enter and leave a trigger. Unfortunately, there is no function available like collider.GetCollidersInsideTrigger(). Fear not, though, it’s fairly easy logic. The following script will track all objects that enter, and store them in a list based on their layer:

#pragma strict
@script AddComponentMenu(“Library/Physics/Trigger Track By Layer”)

/**
* Keep a list of all objects inside this trigger, available by layer
*
* Layer -1 is a list of all objects
*/

// should we track triggers too?
var trackOtherTriggers:boolean = false;

// objects we aren’t tracking
var ignoreColliders:Collider[];

// hashtable of arraylists for our objects
private var objects:Hashtable = new Hashtable();

/**
* Initialize internals
*/

function Awake()
{
   // -1 is all layers
   objects[1] = new ArrayList();
}

/**
* Return an arraylist for a layer–if no layer, just make an empty one
*/

function GetObjects(layer:int):ArrayList
{
   var list:ArrayList = objects[layer];
   
   // scrub any nulls when scripts request a list
   if(list)
   {
      Helpers.ClearArrayListNulls(list);
      return list;
   }
   else
      return new ArrayList();
}

/**
* Record when objects enter
*/

function OnTriggerEnter(other:Collider)
{
   if(!trackOtherTriggers && other.isTrigger)
      return
   
   // is this collider in our ignore list?
   for(var testIgnore:Collider in ignoreColliders)
      if(testIgnore == other)
         return;
   
   var go:GameObject = other.gameObject;
   var layer:int = go.layer;
   
   // create our list if none exists
   if(!objects[layer])
      objects[layer] = new ArrayList();
      
   var list:ArrayList = objects[layer];
   
   // add if not present
   if(!list.Contains(go))
      list.Add(go);
      
   // add to all
   list = objects[1];
   if(!list.Contains(go))
      list.Add(go);
}

/**
* Update when objects leave
*/

function OnTriggerExit(other:Collider)
{
   if(!trackOtherTriggers && other.isTrigger)
      return
   
   // is this collider in our ignore list?
   for(var testIgnore:Collider in ignoreColliders)
      if(testIgnore == other)
         return
   
   var go:GameObject = other.gameObject;
   var layer:int = go.layer;
   
   
   // remove from all
   var list:ArrayList = objects[1];
   list.Remove(go);
   
   // Remove from layer’s list if it’s present
   if(objects[layer])
   {
      list = objects[layer];
      list.Remove(go);
   }
}

/**
* Spew our list as an overlay in debug mode
*/

function OnGUI()
{
   if(!Global.use.debug)
      return;
   
   var debug:String = “”;
   
   for(var de:DictionaryEntry in objects)
   {
      var list:ArrayList = de.Value;
      var layer:int = de.Key;
      
      if(layer == –1)
         continue;
      
      debug += String.Format(“{0} : {1}n”, LayerMask.LayerToName(de.Key), list.Count);
   }
   
   var screen:Vector3 = Camera.main.WorldToScreenPoint(transform.position);
   GUI.contentColor = Color.red;
   GUI.Label(new Rect(screen.x, Screen.height – screen.y, 256, 128), debug);
}

An object destroyed inside of a trigger will not send OnTriggerExit events, so we must scrub our list of any null values before returning it.

Rather than poll this list continuously, our game logic script only checks for an update when things may have changed–when something enters or exits our trigger (the same trigger, as both the generic tracker script and our game logic script are on the same object). We separate scripts like this in order to keep the tracking script generic and reusable between projects. It is easy for our gameplay script to ask the tracker script for all of the objects in our BuildingPieces layer, for instance:

// should we do a calculation next update?
var isDirty:boolean = false;

// cache our tracker reference
private var tracker:TriggerTrackByLayer;
tracker = GetComponent(TriggerTrackByLayer);

/**
* Recalculate status every object
*/

function OnTriggerEnter()
{ 
   isDirty = true;
}
function OnTriggerExit()
{
   isDirty = true;
}

/**
* Check for dirty every FixedUpdate (after OnTrigger* calls)
*/

function FixedUpdate()
{
   if(isDirty)
   {
      // do our game logic, ie:
      var objectsInside = tracker.GetObjects(someLayerNumber);
   }
   
   // unset to prevent logic every frame
   isDirty = false;
}

We use another variant that only tracks objects that match a certain LayerMask, which lets you avoid the overhead of tracking all objects in and out. The example project includes both scripts.

By the way, visualizing the status of your triggers via Gizmos is a very useful debugging tool. In the crane wars example you can draw box colliders like:

/**
* Debug display of our trigger state
*/

function OnDrawGizmos()
{
   // set to whatever color you want to represent
   Gizmos.color = Color.green
   
   // we’re going to draw the gizmo in local space
   Gizmos.matrix = transform.localToWorldMatrix;   
   
   // draw a box collider based on its size
   var box:BoxCollider = GetComponent(BoxCollider);
   Gizmos.DrawWireCube(box.center, box.size);
}

Trigger Tips and Caveats

This article mentions a few already, but to recap:

  • Other triggers will “collide” with triggers! Use this for invisible triggers that don’t collide with your actual physics (spawn point triggers, etc).
  • Triggers do respond to raycasts! Make sure your triggers are set to ignore raycasts, unless you really want to raycast against them.
  • An object destroyed inside of a trigger will not fire OnTriggerExit. If you track objects be wary of this. The only sure way to count objects currently inside of a trigger is to use OnTriggerStay
  • Triggers are fast! Use them! Even in non-physical games you will see speed increases. For example, you could have a tower defense game where your enemies are kinematic rigidbodies and turrets track targets in range using triggers.
  • There is a penalty for moving static colliders (a Collider with no Rigidbody component). If you want to move your trigger around, add a Rigidbody and set it to be kinematic.
  • Triggers are great on the iPhone, since everything happens in highly optimized PhysX. You’d be surprised.

Example Project

The above-mentioned scripts are included in a quick example project (so don’t worry about copying and pasting off the post). Download the example project and check them out!

Other Uses?

How have you guys been using triggers? Share your own tips and tricks in the comments!

This entry was posted in Code Snippets and tagged . Bookmark the permalink.

3 Responses to Getting Tricky with Triggers

  1. Great post! Before reading this, I wasn’t really sure that using colliders was the optimal approach to keeping track of objects that are “near” (versus computing the square of the distance) – it just seemed like the “Unity way” of doing things, and it felt right. :)

    Regarding other uses for triggers, I’m working on a prototype wherein a “bomb” is instantiated on the scene, and destroys everything that is within a certain radius from it. To do this, I instantiate a sphere collider with the same position as the bomb at the time of the explosion, and a script is attached to this that does a “yield WaitForSeconds(epsilon)”, where “epsilon” is a small time value (1/8 of a second or so) representing the effectivity time of the explosion. Then I have an OnTriggerEnter that destroys the entering game objects. Sorry if my description isn’t so clear, but I hope you got the idea. :)

    Thanks for the article and for sharing all this Unity knowledge through all the posts so far, and I’m looking forward to future posts! :)

  2. Mark Johnson says:

    Great thanks. With unity 3 custom layers, trigger become even more awesome.

  3. Mike Stramba says:

    I’m a beginner with Unity.

    I’m trying to add a “start-finish” line to the demo car-race game.

    I’ve added a cylinder, and resized it to stretch across the road, then buried it, with just a bit exposed.

    When the car crosses it, the trigger is firing twice for some reason.

    Any ideas why?

    Mike

Comments are closed.