03 — The 3D Enemy

Run the 3D demo, then open EnemyAI3D.cs and TargetSensor.cs side by side. Select the Enemy in the Hierarchy while playing: you'll see the yellow vision sphere with its cone edges, the red reach sphere, and the cyan patrol route — the whole design, drawn live.

The two detectors

The enemy carries two TargetSensor components, configured differently:

Vision ("the eyes") Attack ("the reach")
Radius 12 2.2
Cone 110° 360°
Line of sight required off
Question it answers "Can I see the player?" "Is the player within reach?"

One reusable component, two Inspector configurations — no code duplication. CanDetect() runs the three classic tests from 01 - Enemy AI Concepts cheapest-first: range (a distance compare), FOV (Vector3.Angle against the cone), then line of sight (Physics.Linecast from eye height to the player's chest, blocked by anything solid that isn't the player).

Sneak test: walk straight at the enemy's back — nothing happens until you cross into the cone or touch the reach sphere. That's the FOV doing its job.

The state machine, state by state

Patrol — walks the waypoint loop at patrolSpeed, idling waitAtPoint seconds at each. Movement is just agent.SetDestination(...); arrival is !agent.pathPending && agent.remainingDistance <= agent.stoppingDistance. Transition out: visionSensor.CanDetect(player) → Chase.

Chase — agent.speed = chaseSpeed and re-SetDestination(player.position) every frame (the NavMesh reroutes around walls automatically — you never wrote pathfinding). Transitions out: reach sensor detects → Attack; or the lose-sight timer fills → back to Patrol. The timer only accumulates while vision fails, and resets the moment the enemy glimpses you again — that 3-second "memory" is what makes hiding behind a wall a real mechanic instead of an instant off-switch.

Attack — agent.ResetPath() (stand still), turn to face the player, and Strike() every attackCooldown seconds. Strike() is deliberately just a Debug.Log plus a red DamageFlash on the player — it's the marked hook where a real game deals damage. Transition out: the reach sensor loses the player → Chase.

Returning to patrol is not a fourth state — EnterPatrol() picks the nearest waypoint and the agent simply walks back. A nice class discussion: when would you want a distinct ReturnHome state? (Answer: when returning has its own rules — holstering weapons, different speed, ignoring the player entirely.)

Design details worth pointing out to students

Exercises

  1. Add a second enemy (duplicate, move it, give it its own patrol points). Notice everything just works — the FSM has no global state.
  2. Give the vision sensor a 60° cone and 20 radius: congratulations, it's a sniper sentry. Then 360°/6: a motion detector. Sensors-as-data means enemy variety is Inspector work, not code work.
  3. Add a Search state: on losing sight, walk to the last seen position and wait 2 s before giving up (store player.position each frame that vision succeeds). This is the single most impactful upgrade — see 06 - Tuning and Extending.
  4. Wire Strike() into a real health system (combine with the UI Toolkit kit's HealthScoreSystem if you have it).

Next: 04 - A-Star for 2D Top-Down