Detecting obstacles along a line

Tutorial on how to use linecasting to detect if there is line of sight between two points.

Contents

Introduction

Raycasting and linecasting are common in games to detect obstacles in the world. Often one will query the colliders in the scene, using for example Unity's Physics class. However, when using this package, it is also possible to query the navmesh for obstacles.

Simple linecasting

The most basic form of linecasting is to check if there is a straight path between two points. This can be done using the AstarPath.Linecast method:

var start = transform.position;
var end = start + Vector3.forward * 10;
if (AstarPath.active.Linecast(start, end)) {
Debug.DrawLine(start, end, Color.red);
} else {
Debug.DrawLine(start, end, Color.green);
}
You can also query a specific graph. However this graph needs to implement the IRaycastableGraph interface (which grid graphs, navmesh graphs and recast graphs do). This slightly faster, and it also allows you to get more information about the hit (see next section).

var gg = AstarPath.active.data.gridGraph;
bool anyObstaclesInTheWay = gg.Linecast(transform.position, enemy.position);

Advanced linecasting

You may want to get more information about the hit, such as the exact point where the line hit the obstacle. Or get information about which nodes the line passed through on its way to the target.

This can be done by querying a specific graph and using an overload that outputs a GraphHitInfo.

var graph = AstarPath.active.data.recastGraph;
var start = transform.position;
var end = start + Vector3.forward * 10;
var trace = new List<GraphNode>();
if (graph.Linecast(start, end, out GraphHitInfo hit, trace, null)) {
Debug.Log("Linecast traversed " + trace.Count + " nodes before hitting an obstacle");
Debug.DrawLine(start, hit.point, Color.red);
Debug.DrawLine(hit.point, end, Color.blue);
} else {
Debug.Log("Linecast traversed " + trace.Count + " nodes");
Debug.DrawLine(start, end, Color.green);
}
The GraphHitInfo struct contains the following data:

origin

Start of the segment/ray.

point

Hit point.

node

Node which contained the edge which was hit.

tangentOrigin

Where the tangent starts.

tangent

Tangent of the edge which was hit.

distance

Distance from origin to point.

Line of sight between two nodes

On grid graphs, there is a convenience function for checking for line of sight between the centers of two nodes:

var gg = AstarPath.active.data.gridGraph;
var node1 = gg.GetNode(2, 3);
var node2 = gg.GetNode(5, 7);
bool anyObstaclesInTheWay = gg.Linecast(node1, node2);

Use case: charge attacks

Assume you have an enemy with a charge attack. When it sees the player, and is close enough, you want it to quickly run towards the player. However, you don't want the enemy to charge through walls or other obstacles. Something like this pseudocode:

if (enemy is close enough && there is a straight path to the player) {
charge towards the player
}

Here's how it will look:

Using Unity's Physics raycasting is not enough here. The enemy could be standing on the other side of a ravine, and a physics raycast would not detect that. Instead, we can use the navmesh to detect if there is a clear path to the player. Putting everything together into a working script, we get something like this:

using UnityEngine;
using System.Collections;
using Pathfinding;

namespace Pathfinding.Examples {
public class ChargeBehaviour : MonoBehaviour {
bool isCharging = false;
public float maxChargingDistance = 10;
public float chargeDuration = 0.5f;
FollowerEntity ai = null;

void Awake () {
ai = GetComponent<FollowerEntity>();
}

void Update () {
// Check if target is close (but not too close) and there is a straight path to it
bool reasonableDistance = ai.remainingDistance< maxChargingDistance && ai.remainingDistance > maxChargingDistance * 0.1f;
bool lineOfSight = !AstarPath.active.Linecast(ai.position, ai.destination);
if (!isCharging && reasonableDistance && lineOfSight) {
// Charge the player
StartCoroutine(Charge(ai.destination));
}
}

IEnumerator Charge (Vector3 point) {
isCharging = true;
var start = ai.position;

// Disable the agent's own movement while charging
ai.canMove = false;
ai.rotation = Quaternion.LookRotation(point - ai.position);
// Override the velocity, in case some animation is using this value to adjust the animation speed
ai.velocity = (point - start) / chargeDuration;

// Move towards the target point over chargeDuration seconds
// In a real game you'll want to do something more sophisticated than this
for (float t = 0; t < chargeDuration; t += Time.deltaTime) {
Debug.DrawLine(start, point, Color.red);
transform.position = Vector3.Lerp(start, point, t / chargeDuration);
yield return null;
}

ai.velocity = Vector3.zero;
yield return new WaitForSeconds(1f);

// Enable the agent's movement again
ai.canMove = true;
isCharging = false;
}
}
}