Lesson 5: Enemy Behaviors and Encounter Design

Your player moves well. Now the world needs opposition that feels intentional, not random. This lesson adds one enemy archetype with a tiny state machine, places it in a first real encounter, and keeps telegraphing clear so difficulty comes from decisions, not confusion.

Lesson Objective

By the end of this lesson you will have:

  1. A EnemyController (or similarly named) script with explicit states such as Idle, Chase, and Attack
  2. A movement approach chosen for your game (NavMeshAgent or lightweight steering toward the player)
  3. One encounter volume or spawn point that introduces the enemy with space to react

Why This Matters

Bad enemy code spreads through every level. If AI is a tangle of if statements inside Update, you will fear adding new modes later. A small state pattern plus layer masks for sight and damage keeps systems debuggable when you add UI, audio, and VFX in upcoming lessons.

Step-by-Step

Step 1: Define the enemy contract

Write three sentences in your design notes:

  • Role – melee bruiser, ranged kiter, or swarm minion
  • Win condition for the player – dodge window, spacing, or priority target
  • Fail state – what should never happen (for example zero wind-up on a one-shot)

Keep Lesson 5 to one role. You can clone the pattern for variants later.

Step 2: Add physics and perception scaffolding

  1. Create a prefab Enemy_Grunt with Collider (trigger or non-trigger based on your hit rules).
  2. Add a child empty Perception for Line of Sight origin (eye height).
  3. Assign layers: Enemy, Player, Default, and optional Obstacle for raycasts.

Pro tip: Put max sight range, attack range, and attack cooldown in a ScriptableObject or serialized fields at the top of the inspector so designers can tune without opening code.

Step 3: Implement a minimal state machine

States for Lesson 5:

  • Idle – patrol or stand (even WaitForSeconds on a coroutine is enough)
  • Chase – move toward the player when in range or on aggro
  • Attack – play a short wind-up, apply damage once, then return to Chase or Idle

Use either:

  • enum EnemyState { Idle, Chase, Attack } with a switch in Update, or
  • Separate small methods EnterChase(), TickChase(), ExitChase() if you prefer explicit transitions.

Common mistake: Calling Destroy(player) in a tutorial joke. Always gate damage through a player health component or event so UI and save systems can subscribe later.

Step 4: Choose movement technology

NavMeshAgent – Great for grounded enemies on walkable floors. Bake a NavMesh on your play space, add NavMeshAgent to the enemy, and set destination to the player position on an interval (not every frame if you have dozens of agents).

Rigidbody / manual steering – Great for simple 2.5D or arcade projects. Move with Vector3.MoveTowards or add force toward the player, clamp speed, and respect slopes if you use gravity.

Pick the path that matches Lesson 4’s motor. If the player is CharacterController-based on tight arenas, NavMesh enemies usually snap cleanly to the same floors.

Step 5: Line of sight and aggro

  1. On a fixed tick (for example 0.2s), cast from Perception to the player’s chest transform.
  2. Use Physics.Raycast with a LayerMask that ignores triggers you do not care about.
  3. When the ray hits the player collider first, set aggro true. When broken for several ticks, drop aggro if you want escape gameplay; otherwise keep it simple and stay aggressive once spotted.

Pro tip: Draw Debug.DrawRay in development builds so you can see why an enemy refuses to wake up.

Step 6: Attack telegraphing

  1. Before applying damage, play a short wind-up (animation event, timer, or scale punch).
  2. Use Physics.OverlapSphere or a hitbox child collider enabled only during active frames if you want precise melee arcs.
  3. Apply damage once per swing using a flag reset when returning to Chase.

Readable attacks beat clever attacks. Players should lose because they misread timing, not because the hitbox was invisible.

Step 7: Build the first encounter

  1. Mark an Encounter_A zone in the level with enough forward space for the player to see the enemy before contact.
  2. Spawn or place one enemy. Avoid clutter until damage feedback exists.
  3. Add cover or width so the player can circle-strafe using Lesson 4 movement.

Document in your task board: Encounter 1 goal – teach spacing and the attack cadence.

Step 8: Hook into your core loop

Reuse whatever win/lose or score stub you built in Lesson 3. When the enemy dies, increment a counter or drop a pickup placeholder. You are wiring cause and effect early so later lessons (UI, save, audio) have events to listen for.

Mini Challenge

Add a second enemy instance in the same scene with different cooldown values only (no new code path). Confirm both share one prefab and behave consistently. If tuning one breaks the other, move magic numbers into the ScriptableObject config.

Pro Tips

  • Prefer events (OnEnemyDied) over static singletons for listeners; your HUD lesson will thank you.
  • Keep rotation toward the player smooth with Quaternion.Slerp for readability on melee swings.
  • Log state transitions with [Conditional("UNITY_EDITOR")] helpers during development, then strip noise before builds.

Common Mistakes

  • Chase code that sets destination every frame without throttling, tanking performance with many agents
  • Damage in OnTriggerStay firing every physics frame
  • Enemies that snap rotation instantly, hiding the attack facing direction

Troubleshooting

"Enemy walks through walls"

Re-bake NavMesh after geometry changes, or add NavMeshObstacle to dynamic blockers.

"Ray always fails"

Confirm layers, max distance, and that the player collider is on the expected layer.

"Attack never lands"

Check that hitbox timing overlaps the player collider and that one side uses a trigger if you rely on OnTriggerEnter.

Recap

You implemented a compact enemy state machine, connected perception and movement, and framed a first encounter that respects your input architecture.

Next Lesson Teaser

Lesson 6 connects menus, HUD, and feedback so players see health, prompts, and results instead of guessing what your systems are doing.

FAQ

Do I need behavior trees in Lesson 5?
No. A handful of states stay easier to ship and debug for a vertical slice.

Should enemies use the same Input System as the player?
Usually not. AI generates intent in code; players generate intent from devices.

What about object pooling?
Note it on the backlog. Add pooling when spawn counts rise in later lessons.

Related Links

Save this lesson when you start tuning attack wind-ups. Fair combat is mostly clarity, not raw difficulty.