Friday, 3 April 2015

More Unity Raycasting woes

This Raycasting malarky was supposed to make things simple.
And if it behaved the way we understand it to, things like line of sight (for placing pieces, showing and hiding pieces, and for shooting at enemies) would be a doddle. But it's not.

It's really confusing.
And there's not much clue as to why sometimes our line-of-sight works, and sometimes doesn't.

To create a line of sight, we select a source square (the place you're travelling from) and a destination square (where you want to move to). We create two temporary objects (I created cubes just so they would appear on screen, but they can be empty gameobjects) and place them in the centre of the source and destination squares.



We turn the squares to face each other, so that their rotation represents the direction of travel. When we cast a ray from the source point, in the direction of the target, using

var ray:Ray=new Ray(v1.transform.position, (v2.transform.position- v1.transform.position) );
a = Physics.RaycastAll(ray, dist);
return(a);

The full function is here:

function objectsBetweenSquaresOffset(boardID_start:int, squareNo_start:int, boardID_end:int, squareNo_end:int, xOffset:float):RaycastHit[]{

     // this function takes the centre point between squares and
     // then offsets to the left/right (and to the opposite side on the target)
     // returning anything along the path between the two points
     
     var pt1:point2D=getPointFromBoard(boardID_start, squareNo_start);
     var pt2:point2D=getPointFromBoard(boardID_end, squareNo_end);
     var a:RaycastHit[]=null;

     sourceObject.transform.position.x=pt1.x*2;
     sourceObject.transform.position.z=pt1.z*2;     
     sourceObject.transform.position.y=1.4;
          
     targetObject.transform.position.x=pt2.x*2;
     targetObject.transform.position.z=pt2.z*2;
     targetObject.transform.position.y=1.4;
     
     sourceObject.transform.LookAt(targetObject.transform.position);
     targetObject.transform.LookAt(sourceObject.transform.position);

     // parent the objects to the current character (so when we increase the x values
     // it's relative to the way the character is facing, not just global increase)
     var v1:GameObject=new GameObject();
     var v2:GameObject=new GameObject();
     
     v1.transform.position=sourceObject.transform.position;
     v1.transform.rotation=sourceObject.transform.rotation;
     v1.transform.parent=sourceObject.transform;
     
     v2.transform.position=targetObject.transform.position;
     v2.transform.rotation=targetObject.transform.rotation;
     v2.transform.parent=targetObject.transform;
     
     // now offset the objects either left or right, just a bit
     v1.transform.localPosition.x+=xOffset;
     v2.transform.localPosition.x-=xOffset;

     v1.transform.parent=null;
     v2.transform.parent=null;

     // create a ray from the source object to the target object
     var dist:float = ((pt1.x-pt2.x)*(pt1.x-pt2.x)) + ((pt1.z-pt2.z)*(pt1.z-pt2.z));
     dist=Mathf.Sqrt(dist);
     var c:Color=Color.red;
     if(xOffset<0){c=Color.green;}
     trace("distance to destination: "+dist);
     
     // we tried this and it made no difference
     //var direction:Vector3 = ( v2.transform.position - v1.transform.position ).normalized;
     //var ray:Ray=new Ray(v1.transform.position, direction );
     
     var ray:Ray=new Ray(v1.transform.position, (v2.transform.position- v1.transform.position) );     
     Debug.DrawRay(v1.transform.position, (v2.transform.position- v1.transform.position), c, 30);
     a = Physics.RaycastAll(ray, dist);
     Destroy(v1);
     Destroy(v2);     
     return(a);
}


It's called like this:
The idea is that we assume we're always going to perform a valid move. Then we pass in the boardid and square number of the playing piece (current square) and the boardid and square number of the target square into the function above.


var validMove:boolean=true;

// first check that there's a line of sight between the two squares
// (you're not trying to walk through a wall or a door or something)

var hits1:RaycastHit[]=objectsBetweenSquaresOffset(pieceInHand.curr_boardid, pieceInHand.curr_squareno, boardID, squareNo, 0.8f);
var hits2:RaycastHit[]=objectsBetweenSquaresOffset(pieceInHand.curr_boardid, pieceInHand.curr_squareno, boardID, squareNo, -0.8f);

if(hits1){trace("hits1 returned "+hits1.Length+" collisions");}else{trace("no hits1");}
if(hits2){trace("hits2 returned "+hits2.Length+" collisions");}else{trace("no hits2");}

// first check out hits along the left-hand side
if(hits1){
     for (var i:int=0; i<hits1.Length; i++) {
          var hit1 : RaycastHit = hits1[i];
          if(hit1.collider.gameObject.transform.root.transform==activeCharacter.transform){
               // ignore this, you've just collided with yourself!
               trace("ignore collision with self");
          }else{
               var s1:String=hit1.collider.gameObject.tag;
               // if you hit anything along the way, the way is blocked
               trace("i) path blocked by "+s1+" "+hit1.collider.gameObject.transform.root.name);
               validMove=false;
          }
     }
}

// now hits along the right-hand side
if(hits2){
     for (var j:int=0; j<hits2.Length; j++) {
          var hit2 : RaycastHit = hits2[j];     
          if(hit2.collider.gameObject.transform.root.transform==activeCharacter.transform){
               // ignore this, you've just collided with yourself!
          }else{
               var s2:String=hit2.collider.gameObject.tag;
               // if you hit anything along the way, the way is blocked
               trace("ii) path blocked by "+s2+" "+hit2.collider.gameObject.transform.root.name);
               validMove=false;
          }
     }
}


The collision function converts the current boardid and square number into a pair of points (and does the same for the target square). This function is included below for completeness, but it's accuracy is not in question: the points returned are used to draw the ray on the screen, so we can see that they are correct.

We then place our sourceObject cube and our targetObject cube in the appropriate places on the map. Again, these can be seen as in the correct places, in the screenshot, so there is little question of this being incorrect. We turn the two cubes to look at each other.

We then create a blank object and place this as a child of the sourceObject (and create a blank child for the targetObject too). Then, we shift these blank objects a little to one side (depending of the value of the xOffset parameter passed into the function) by changing their local x co-ordinate (if we changed their global x co-ordinate, the line would not necessarily begin to the side of the cube: local co-ordinates take into consideration the rotation of the cubes when shifting the starting points).

Using a simple Pythagoras equation we work out the distance between the two cubes, and then create a ray between them. We then use Physics.RaycastAll to return a list of all objects collided with along the length of the ray.

The strange thing is, if the point of collision is less than half-way along the ray, the ray successfully detects an obstacle. But if, for example, we place our character two squares away from a wall, and select the target square just one square away from the wall, on the opposite side, something peculiar happens:


Where the obstacle intersects the ray at any point more than half-way along, the box collider is not detected. In the example above, the ray is drawn passing through the door (which has a number of box colliders on it, but we get the same result with a wall section with just one collider) but this time, RaycastAll does not return the obstacle.

The problem is surely not with the box collider on the obstacle, since it is sometimes correctly identified. It only seems be by how much the ray passes through the obstacle that determines whether or not the box collider is detected.

The problem is, we're not sure whether we're not understanding how box colliders work, whether there's a bug in the code that we're just not seeing, whether or not the box collider is set up correctly, or whether there's a bug with Unity and using box collidors on objects that are instantiated in code (online sources suggest that Unity has a bug which can be squashed by disabling then re-enabling an object after instantiating, but even with this in place, we're getting the same results).

A read head-scratcher this one........