Detecting obstacles along a line
Tutorial on how to use linecasting to detect if there is line of sight between two points.
Contents
- Introduction
- Simple linecasting
- Advanced linecasting
- Line of sight between two nodes
- Use case: charge attacks
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;
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 end = start + Vector3.forward * 10;
if (AstarPath.active.Linecast(start, end)) {
Debug.DrawLine(start, end, Color.red);
} else {
Debug.DrawLine(start, end, Color.green);
}
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;
The GraphHitInfo struct contains the following data:
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);
}
Start of the segment/ray.
Hit point.
Node which contained the edge which was hit.
Where the tangent starts.
Tangent of the edge which was hit.
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;
}
}
}