// 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(); agent = mover.GetComponent(); 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; } }