28 July 2013

Area (concave) triggers

One issue with physics triggers in game engines is that the volumes need to be convex. Say you have a river with lots of turns and curls. You may want to play a water sound effect when the player is near the water. It is impossible to create a trigger that matches the shape of the river, so you place multiple convex (box?) triggers all over the river, enabling the same sound. If it's a long and complex river, this can be a lot of work.

This is of course only one example, perhaps you have a complex dungeon with lots of turns, where you want triggers around the turns. You can create a box volume that envelops a complete turn, but that might overlap with other parts of the dungeon. You might create multiple volumes fitting the corridor and reporting their events to another single game object but that can get messy easily. It's also less efficient.

The engine of Divinity II uses NVidia's PhysX, so back at Larian we faced the same restriction. For that engine I introduced the concept of area triggers, an idea based on concepts that I used in my master thesis. Now I'm working on a game made in Unity and ran into a similar problem so I decided to rewrite the idea in Unity.

Problem at hand

In Unity, you can have a concave mesh collider as trigger, but it will only generate events for character controllers or rigid bodies with a sphere, capsule or box collider. Also, it will generate exit and enter events when the object passes through the surface of the trigger, and not the volume. So if a sphere passes through a concave trigger volume it will generate four events: an enter when it first touches the surface, an exit when the object does not touch that surface anymore (but the sphere is still inside the volume) and then another enter and exit when the sphere passes through surface of the trigger on the other side.

What we desire is a single enter and exit event, and correct stay events.

Proposed solution

In 2D, we have an algorithm that can decide whether a point is inside a polygon or not. The algorithm can be found here. So what I suggest here is that we create so called 'areas', existing out of a 2D polygon and a top and a bottom. To determine whether a point is inside the volume defined by the area, we simply need to check whether it's between top and bottom (easy out) and inside the polygon. When the physics trigger sends an exit/enter event we can now check whether it's valid. For an exit event we need to check if the position of the given collision object is inside the area. If it is, we remember the object as still inside in a list (the physics engine however believes it's not). For an enter event we need to check whether the collider is already registered and if so, simply remove it from the list.

This means there is only a small overhead we introduce for the exit event.

Code

The snippet below illustrates the idea. When the OnTriggerExit method is called, it is possible that the collider 'other' is still inside the area. If so, it is added in the 'insideObjects' list, else it is a genuine exit event and the message is translated into 'OnAreaTriggerExit'.

If an OnTriggerEnter event is received we first check if it's not registered as already inside. If not the event is forwarded as 'OnAreaTriggerEnter', else it is removed from the insideObjects list.

public class AreaTrigger : MonoBehaviour
{
 ...
   
 public void OnTriggerEnter (Collider other) {
  if(!insideObjects.Contains(other))
   this.gameObject.SendMessage("OnAreaTriggerEnter",other, SendMessageOptions.DontRequireReceiver);
  else
   insideObjects.Remove(other);
 }
 
 public void OnTriggerExit (Collider other) {
     if(ContainsPoint(other.transform.position))
   insideObjects.Add(other);
  else
   this.gameObject.SendMessage("OnAreaTriggerExit",other, SendMessageOptions.DontRequireReceiver);
 }
 
 public void OnTriggerStay (Collider other) {
  this.gameObject.SendMessage("OnAreaTriggerStay",other, SendMessageOptions.DontRequireReceiver);
 }
 
 public void Update() {
  foreach(Collider other in insideObjects)
   this.gameObject.SendMessage("OnAreaTriggerStay",other, SendMessageOptions.DontRequireReceiver);
 }
 
 ...
}

Creating these areas

While this idea turns out to be working just fine, we still need to be able to create these 'areas'. For that I wrote a custom editor. You simply create an empty gameobject and attach the area trigger script. All needed components will be added. With the game object selected, control clicking in the scene will add points to the area forming the polygon. Once you have three or more points a blue volume will be rendered. The points that make up the area have handles, so you can move the points of the polygon around (don't forget the 'Display Handles' option in the Inspector). You can also add and edit points in the inspector. To hide the blue volume simply disable the renderer.

The screenshot below illustrates how this looks.

Source

The source code of the area trigger class and it's editor script, all bundled up in a demo project has been placed on bitbucket. I'm open to all possible improvements and suggestions.

You simply open this demo and hit the play button, a sphere will start turning round through the area trigger and in the log you'll see the correct area trigger events being fired.

It's all open source and no license etc, but I'd like to know if you used this, just post me a comment or send me an e-mail. Questions are welcome!

2 comments:

Senshi said...
This comment has been removed by the author.
Senshi said...

Nice work and great idea! I unfortunately won't be using it for now, but your post gave me some great ideas of my own. =) Thanks for the write-up, and of course for sharing the source!