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;
    }
}