Back to projects
AIController.csC# · Unity

Metal Gear Mechanics

A recreation of Metal Gear Solid's stealth loop in Unity. Guards patrol set routes, notice the player through a vision cone backed by a line-of-sight check, and go through scan and search states before returning to patrol. It also has CQC, the cardboard box, and wall cover.

NavMeshVision conesLine of sightNo per-frame allocationObject pooling

My role

Solo project. I wrote all of the gameplay code: the AI, items and inventory, the managers, scene loading, UI, and helpers like the object pooler. The environment art, particle effects and the PS1-style post-processing are asset packs.

The problem

Every guard has to check what's near it every frame. Unity's basic Physics.OverlapSphere returns a brand new array every time you call it, so with a lot of guards you're making garbage every frame, and eventually the garbage collector pauses the game to clean it up. I wanted detection that makes zero garbage no matter how many guards are running.

How it works

  • All guards share one static Collider[16] array instead of each one allocating its own.
  • Physics.OverlapSphereNonAlloc fills that array and returns a count, so nothing new gets allocated per check.
  • A layer mask limits the check to one layer, so physics skips everything else.
  • A vision cone check with Vector3.Angle throws out anything outside the guard's field of view before any raycast happens.
  • Physics.Raycast then confirms line of sight, but only for what passed the cone check, since raycasts cost more.
  • The guard's behavior is split into five state classes (patrol, caution, search, combat and grabbed), each in its own file, and NavMesh handles walking between waypoints.

Trade-offs

  • The shared array holds 16 colliders. Anything past 16 in range gets ignored. My levels never put that many things near one guard, but it's a hard limit.
  • Because every guard uses the same array, detection can't be split across threads without changing that. It doesn't matter here, since Unity runs Update on the main thread anyway.
  • Every guard checks every frame instead of taking turns across frames, so the total cost grows with the number of guards.

What I measured

6 guards0.03 ms0 B allocated
51 guards0.63 ms0 B allocated
215 guards3.19 ms0 B allocated
584 guards8.70 ms0 B allocated

Measured in the Unity 2022.3 editor on an RTX 4080 SUPER and Ryzen 7 9800X3D, reading AIController.Update in the Unity Profiler. It works out to about 0.015 ms per guard, and allocation stayed at 0 B at every guard count. At 584 guards the AI was 21% of a 41 ms frame. The biggest cost at that point was rendering the animated guard models, not the AI.