Source file unity-csharp/game-ai/navmesh-steering/SoldierAI.cs from the GDnD code examples. Download raw file
// GDnD wiki example
// Demonstrates: the hand-off from decision to motion. A behaviour tree decides (take cover > chase > patrol),
// a tactical query picks a cover point and COMMITS to it, and NavMover turns each choice into smooth movement.
// Leaves set a destination and return Running until the agent arrives; they never stop the agent between targets.
// Related pages: [[unity-navmesh]], [[behaviour-trees]], [[tactical-position-selection]], [[utility-ai]]
// Requires: BehaviourTree.cs from ../behaviour-tree/ in the same project, NavMover.cs, and a baked NavMesh.
using UnityEngine;
using UnityEngine.AI;
[RequireComponent(typeof(NavMover))]
public class SoldierAI : MonoBehaviour
{
[SerializeField] private Transform player;
[SerializeField] private Transform[] coverPoints;
[SerializeField] private Transform[] patrolPoints;
[SerializeField] private LayerMask wallMask; // what blocks line of sight
[SerializeField] private float sightRange = 15f;
[Header("Under fire (toggle in Play mode)")]
[SerializeField] private bool underFire;
[SerializeField] private float coverRecheckInterval = 0.5f;
[Header("Debug (read only)")]
[SerializeField] private string currentAction;
private NavMover mover;
private NavMeshAgent agent;
private BTNode root;
private Transform chosenCover; // the committed decision
private float nextCoverCheck;
private int patrolIndex;
private NavMeshPath scratch;
private void Awake()
{
mover = GetComponent<NavMover>();
agent = mover.GetComponent<NavMeshAgent>();
scratch = new NavMeshPath();
root = new SelectorNode("Root",
new SequenceNode("Take cover when under fire",
new ConditionNode("Under fire?", () => underFire),
new ActionNode("Choose cover", ChooseCover),
new ActionNode("Move to cover", MoveToCover)),
new SequenceNode("Chase when seen",
new ConditionNode("Can see player?", () => CanSee(transform.position, player.position)),
new ActionNode("Chase", Chase)),
new ActionNode("Patrol", Patrol));
}
private void Update()
{
if (!underFire) chosenCover = null; // forget cover once the threat has passed
root.Tick();
}
// Tactical query: generate (cover points) -> filter (hidden from the player) -> score (path length).
// Commitment: keep the chosen point until it is compromised, re-checked only every half second,
// so the destination does not flip between two similar candidates every frame.
private NodeState ChooseCover()
{
if (chosenCover != null && Time.time < nextCoverCheck) return NodeState.Success;
nextCoverCheck = Time.time + coverRecheckInterval;
if (chosenCover != null && !CanSee(chosenCover.position, player.position)) return NodeState.Success;
chosenCover = null;
float best = float.PositiveInfinity;
foreach (Transform c in coverPoints)
{
if (CanSee(c.position, player.position)) continue; // filter: exposed
float cost = PathLength(transform.position, c.position); // score: shorter is better
if (cost < best) { best = cost; chosenCover = c; }
}
return chosenCover != null ? NodeState.Success : NodeState.Failure;
}
private NodeState MoveToCover()
{
currentAction = "Move to cover";
agent.autoBraking = true; // stop precisely on the cover point
MoveStatus s = mover.MoveTo(chosenCover.position);
if (s == MoveStatus.Arrived) currentAction = "In cover";
return s == MoveStatus.Failed ? NodeState.Failure : NodeState.Running; // stay in cover while under fire
}
private NodeState Chase()
{
currentAction = "Chase";
agent.autoBraking = false; // don't slow down at each re-path
return mover.MoveTo(player.position) == MoveStatus.Failed ? NodeState.Failure : NodeState.Running;
}
private NodeState Patrol()
{
currentAction = "Patrol";
if (patrolPoints.Length == 0) return NodeState.Failure;
agent.autoBraking = false; // flow through patrol points
if (mover.MoveTo(patrolPoints[patrolIndex].position) == MoveStatus.Arrived)
patrolIndex = (patrolIndex + 1) % patrolPoints.Length;
return NodeState.Running;
}
private bool CanSee(Vector3 from, Vector3 to)
{
Vector3 eye = Vector3.up * 1.5f;
return Vector3.Distance(from, to) <= sightRange && !Physics.Linecast(from + eye, to + eye, wallMask);
}
private float PathLength(Vector3 from, Vector3 to)
{
if (!NavMesh.CalculatePath(from, to, NavMesh.AllAreas, scratch)
|| scratch.status != NavMeshPathStatus.PathComplete) return float.PositiveInfinity;
float length = 0f;
Vector3[] corners = scratch.corners;
for (int i = 1; i < corners.Length; i++) length += Vector3.Distance(corners[i - 1], corners[i]);
return length;
}
}