← All patterns
Optimization

Spatial Partition

Bucket objects by where they are so "what is near me?" only looks at the neighbourhood, not the whole world.

What Is It?

Many game questions are spatial: which enemies are inside this explosion, which boids are close enough to flock with, which pickups can the magnet pull? The naive answer loops over every object and measures the distance — fine for 50 units, ruinous for 5,000 asking the same question each frame.

A spatial partition divides the world into regions and files each object under the region it occupies. The simplest version is a uniform grid: a Dictionary<Vector2Int, List<Unit>> keyed by cell. A query converts its bounds into a range of cells and only inspects objects stored in those cells.

Objects must tell the grid when they move to a different cell, but that is cheap — a remove and an add, and only when crossing a boundary. In return, neighbour queries go from O(n) per query to roughly the handful of objects in nearby cells. Quadtrees, octrees and BVHs are the adaptive cousins of the same idea.

When Is It Used?

Use it when many objects repeatedly query their surroundings: crowd steering, RTS target acquisition, bullet-hell collision, AI perception, proximity voice chat.

A uniform grid works best when objects are spread fairly evenly and have similar sizes. For very clumpy worlds or huge size differences, a quadtree adapts better.

If you only have a few dozen objects, or Unity physics queries like Physics.OverlapSphereNonAlloc already answer the question, the built-in broadphase is probably enough.

Interactive Demo

Click the controls and watch the objects collaborate. The console mirrors what the C# code below would log.

Code

Assets / Scripts / Optimization/ SpatialPartition ›SpatialTester.cs
using System.Collections.Generic;
using UnityEngine;

namespace Patterns.Optimization.SpatialPartition
{
    /// <summary>
    /// Spawns wandering units, then every frame finds those within a radius
    /// of this object using the grid instead of scanning every unit.
    /// </summary>
    public class SpatialTester : MonoBehaviour
    {
        [SerializeField] private Unit unitPrefab;
        [SerializeField] private int unitCount = 500;
        [SerializeField] private float worldSize = 100f;
        [SerializeField] private float cellSize = 10f;
        [SerializeField] private float queryRadius = 8f;

        private SpatialGrid<Unit> grid;
        private readonly List<Unit> results = new();

        private void Awake()
        {
            grid = new SpatialGrid<Unit>(cellSize);

            for (int i = 0; i < unitCount; i++)
            {
                Vector2 p = new(Random.value * worldSize, Random.value * worldSize);
                Unit unit = Instantiate(unitPrefab, new Vector3(p.x, 0f, p.y), Quaternion.identity);
                unit.Init(grid, worldSize);
            }
        }

        private void Update()
        {
            Vector2 center = new(transform.position.x, transform.position.z);
            int checks = grid.Query(center, queryRadius, results);

            foreach (Unit unit in results)
                Debug.DrawLine(transform.position, unit.transform.position, Color.cyan);

            if (Time.frameCount % 60 == 0)
                Debug.Log($"Found {results.Count} with {checks} distance checks (brute force: {unitCount}).");
        }
    }
}
3 files · namespace Patterns.Optimization.SpatialPartitionC# · UTF-8 · LF

Advantages & Disadvantages

+ Advantages

  • Turns neighbour searches from checking everything into checking a small local set.
  • A uniform grid is tiny to implement and has predictable, cache-friendly costs.
  • Works for non-physics data too — AI blackboards, audio emitters, fog-of-war reveals.

− Disadvantages

  • Objects must be kept in sync; forgetting to update a moved unit makes queries silently wrong.
  • Choosing a cell size is a trade-off: too small means many cells per query, too large means crowded cells.
  • Uses extra memory for buckets, and dense clusters in one cell degrade back towards brute force.

Tips

  1. 01Pick a cell size close to your most common query radius; then a query touches at most a 3×3 block of cells.
  2. 02Only re-bucket a unit when its cell coordinate actually changes — compare the old and new Vector2Int first.
  3. 03Reuse a results List<T> passed into the query instead of returning a new list, to keep the hot path allocation-free.
  4. 04The grid narrows candidates; still do the exact distance check (sqrMagnitude against radius²) on those candidates.
  5. 05For thousands of units, consider a flat-array grid with Burst and the Jobs system instead of a dictionary of lists.