Thursday 20 April 2017

Unity, raycasting and line of sight between objects

We're putting together a simple 2D/top-down game that makes extensive use of "line-of-sight" rules as players move around the game world. There are a few ways you can check for line-of-sight but they almost always involve drawing a line between two points, then seeing which objects (if any) intersect this line.

If we were coding this in any other language for any other system, that's probably how we'd do it anyway; create an equation to describe the line between the two points, then "walk along" the length of the line, one pixel/unit at a time, checking to see if the x/y position of any other object in the gameworld is close enough to the line to be considered intersecting with it.

Unity provides this functionality already with its Physics.Raycast function.

Provide the function with a start point vector and a direction vector and it imagines an infinitely long line (the "ray") from the origin, and returns the first object that the ray collides with (if any). There's also Physics.RaycastAll which does the same thing, but returns an array of all objects hit by the ray.

We made use of the RaycastAll function in our line-of-sight checks (although we're building a 2D game, the same principles of 3D development still exist, we just treat everything as if it were all on the same Z-plane). We put three moving objects into our game world, with one of them hidden behind a wall. We then updated the position of each object and ran our line-of-sight checks from the moving object to all other objects in the world. The checks involved raycasting from the movign object to each other object (in turn) and checking the array of collisions.

If the array was empty, there were no detected obstacles along the line, and so we said that there was a line-of-sight between the two (in future development we'll have to include things like facing and field-of-vision and so on, but for now we're just trying to decide if an obstacle exists between two points). If any obstacle was returned in the array, we said that no line-of-sight existed between the two objects.



On the face of it, a simple Raycast function might do the job, as we're only interested - at this stage - in the binary option of "is there an obstacle between these two points". But we wanted to use the RaycastAll function to return ALL objects so that in future we might be able to assign "visibility" to different obstacles. Some obstacles may, for example, be see-through, but we still want them to act as an obstacle for purposes other than viewing. A classic example might be a glass window: you can see through it but it also acts as a physical barrier.

So we don't just want our line-of-sight function to return false if any old obstacle exists between two points - we want to inspect each obstacle type between the points and decide whether or not to include them in our line-of-sight check. So instead of Physics.Raycast, we used Physics.RaycastAll.

Everything seemed to be working just fine for a while; our hidden object remained hidden and the visible object revealed itself in good time. The function correctly identified whether or not there was a line-of-sight between all of the objects. Then something funny happened - despite there being a pefectly clear run between our first two objects, the LOS  function started returning false



Even more peculiarly, sometimes the function returned true (is there a line of sight between these two objects) and sometimes false, depending on which object we used as the source and which was the destination. Yet as we hadn't yet introduced rotation or facing into our function, it didn't make sense that an obstacle was found if we went from A to B but none were found if we went from B to A.

After much puzzling and re-reading the Unity documentation, we eventually worked out the problem. Our ray was continuing beyond the object being tested. So although we thought were asking "are there any obstacles along a ray between these two points?" the function was actually returning "are there any objects along an infinitely long ray, starting at point A and continuing in the direction towards point B?"



Of course, as soon as we moved an object so that there was a wall behind it, the function found the wall. The ray passed through the second object, struck the wall behind and said "yes, I found an obstacle along that ray".

What we needed to do was limit the length of the ray;
The RaycastAll function has an overload which allows you to enter a start point, a direction and a magnitude (maximum length of the ray). We created our ray be subtracting the gameworld co-ordinates of the source object from the co-ordinates of the destination object. This creates a vector describing the path between the two objects. We use this vector as our ray. Having created the ray, we then used the magnitude of the direction vector as the length of the ray.

As soon as we limited the length of the ray to match the length of the vector describing the direction from one object to the other,the function worked as expected, both "forwards" and "backwards" (i.e. it didn't match which object we used as the source and which was the destination).

bool hasLOS(GameObject source, GameObject dest){
   // firstly cast a ray between the two objects and see if there are any
   // obstacles inbetween (some obstacles have "partial visibility" in which
   // case we may or may not want to include as a "hit")

   RaycastHit[] hits;
   bool obj_hit = false;

   Vector3 dir = dest.transform.position - source.transform.position;
   Ray ry = new Ray ();
   ry.origin = source.transform.position;
   ry.direction = dir;

   hits = Physics.RaycastAll (ry, dir.magnitude);
   Debug.DrawRay (source.transform.position, dir, Color.cyan, 4.0f);

   foreach(RaycastHit hit in hits){
      // here we could look at an attached script (if one exists) on the object and
      // decide whether or not this should actually constitute a hit
      Debug.Log("LOS test hit from "+source.transform.position+" to "+dest.transform.position+" = "+hit.transform.parent.gameObject.name);
      obj_hit = true;
   }

   return(!obj_hit);
}


Within the foreach loop we can put some further testing to decide whether or not the obstacle has an effect. So in the case of firing a bullet at a target which is on the other side of a glass wall, we could call the function and ignore the glass object when testing for line of sight (can we see the object behind the glass) but include the object as an obstacle when using the same function to decide if, say, a bullet were to be fired from one object at another.

The same result could be achieved using trigonometry (lots of tan/cos functions) but Unity does provide lots of nice, easy, helper functions, such as Raycast and RaycastAll. Thanks Unity!


1 comment:

  1. Thanks for this, I'm just starting out with Unity and this was nice, cheers!

    ReplyDelete