Algorithms and Delegates: The Invisible Backbone of Game Code

11 min read
  • Algorithms
  • Delegates
  • C#
  • Unity
  • Architecture

Two parts of game programming get talked about less than they should: algorithms and delegates.

Both are invisible when they work. Both are catastrophic when they don't. And both separate "this game runs at 60fps" from "this game stutters and we don't know why."

I'll cover the algorithms that show up in real game code, the role of delegates (and their cousins: events, Action, Func), and how to use them together without painting yourself into a corner.

Why Algorithms Actually Matter in Games

A lot of devs treat algorithms as a coding interview thing. Game code is supposed to be different — it's all about feel, juice, design.

Wrong. Game code is the most algorithm-dense codebase you'll write outside of a database engine.

Examples from real production:

  • Pathfinding. Every enemy that walks toward the player runs an algorithm — A*, NavMesh queries, flow fields. Wrong choice = stuttering AI.
  • Match detection. Match-3, Block Blast, Connect — every move triggers a search/scan algorithm. Naive impl = 50ms hangs on big boards.
  • Spatial queries. "Find all units within 5 meters of the player." Without spatial partitioning, this is O(n²) and your game dies at 200 units.
  • Sorting. Leaderboards, matchmaking, draw order. Sometimes hot enough to matter.
  • Procedural generation. Levels, terrain, dungeons, item drops. All algorithm.
  • Save serialization. Stable, compact, fast — algorithm choice decides if a save takes 200ms or 2s.

The skill in games isn't writing the algorithm from scratch — it's recognizing which one applies, picking the right complexity class, and knowing the tradeoffs.

The Algorithms That Show Up Most in Game Code

A* (A-Star) Pathfinding

Finds shortest path on a graph using heuristic to prune the search. The standard for grid/navmesh pathfinding.

Use when: Static maps, predictable cost. Skip for: Massive crowds (use flow field) or dynamic terrain (consider HPA*).

BFS / DFS

Breadth-first explores level by level (shortest unweighted path). Depth-first goes deep first (good for puzzles, flood fills).

Use when: BFS for "shortest hops," DFS for connected component / flood fill (think paint bucket, match-3 group find).

Dijkstra

Shortest path with weights, no heuristic. A* without the smart pruning.

Use when: All-source shortest paths, no good heuristic exists. Skip when: A* fits.

Spatial Partitioning (Quadtree, Octree, Grid Hash)

Cut the world into cells. "Find things near X" becomes O(cellSize) instead of O(n).

Use when: >100 dynamic objects with proximity queries. Skip when: Small entity counts.

Behavior Tree / GOAP / FSM

Three flavors of AI decision-making. FSM is simple states. Behavior Tree is a hierarchy of conditions and actions. GOAP plans actions to reach a goal.

Use when: FSM for simple enemies, BT for complex companions, GOAP for emergent AI.

Perlin / Simplex Noise

Generate continuous, natural-looking randomness. Terrain, clouds, organic textures.

Use when: Procedural worlds, terrain heightmaps, ambient effects. Skip when: You need pure RNG.

Wave Function Collapse / BSP

Procedural level generation. WFC fits tile sets together by adjacency rules; BSP recursively splits space.

Use when: Roguelikes, procedural dungeons, tile-based maps.

Bit Manipulation / Bitmasks

Pack flags, states, layers into integers. Collision layers, state flags, dirty bits.

Use when: Many boolean states per entity, hot-path checks. Skip when: Two flags (just use bools).

Hashing

O(1) lookups for prefab IDs, asset names, network entity IDs. The unsung hero of game perf.

Use when: Any "find by key" at scale. Skip when: Linear scan is fast enough.

Memoization / Dynamic Programming

Cache expensive computation results. Pathfinding cache, AI evaluation cache.

Use when: Same input → same output, recomputed often. Skip when: Memory budget is tight or inputs are unique.

Picking Complexity Classes (Quick Reference)

The number that matters most isn't algorithm name — it's complexity class:

  • O(1) — Constant. Hash lookup, array index. Always great.
  • O(log n) — Logarithmic. Binary search, balanced tree. Excellent for big n.
  • O(n) — Linear. Single pass over a list. Fine for n up to ~10k per frame.
  • O(n log n) — Sorting, divide-and-conquer. Fine for moderate n.
  • O(n²) — Quadratic. Naive nested loops. Death after n=200 in a hot path.
  • O(2^n) / O(n!) — Exponential / factorial. Only acceptable for very small n (chess engines, planning).

Rule of thumb: if your hot loop is O(n²) with n > 100, you have a perf problem waiting to happen.

Delegates: The Other Half of the Picture

If algorithms are the math, delegates are the wiring.

A delegate is "a function as a value." You can pass it, store it, call it later, swap it out. In C# (Unity's language) that's Action, Func, Predicate, custom delegate, or event.

This is the difference between "everything calls everything directly" and "systems hand each other phone numbers and wait to be called."

Why Delegates Matter

  • Decoupling. UI doesn't need to know about gameplay. It subscribes to a OnHpChanged event. Gameplay fires it. Either side can change without breaking the other.
  • Async patterns. Coroutines, callbacks for "when this finishes." LoadScene(onComplete: () => StartGame());
  • Strategy injection. Pass a Func<int, int, int> for "how to score" — designers can swap rules without re-shipping a class.
  • Configuration. Inspector hooks (UnityEvent in Unity) let designers wire up behaviors without code.
  • Test surface. A class that takes Action onSpawn is testable. A class that hardcodes EnemyManager.Instance.Spawn() isn't.

The whole event-driven, modern game architecture rests on delegates.

The Delegate Vocabulary (C# / Unity)

Action

Action, Action<T>, Action<T1, T2> — a function returning void.

public event Action<int> OnHpChanged;
OnHpChanged?.Invoke(currentHp);

Use when: Notifying that something happened. Most common shape.

Func

Func<T>, Func<TIn, TOut> — a function returning a value.

public Func<Enemy, float> ScoreSelector;
var best = enemies.OrderByDescending(ScoreSelector).First();

Use when: "How should I compute X?" — strategy injection.

Predicate

Predicate<T> — a function returning bool. Same as Func<T, bool> but reads better for filters.

public Predicate<Tile> IsValidTarget;

Use when: Filtering, validation logic injected by caller.

event

event Action<T> OnSomething — a multicast delegate where only the declaring class can Invoke.

Use when: Public notifications. The event keyword prevents external code from clearing the subscriber list or invoking the delegate.

UnityEvent

Inspector-serializable event. Designers can drag-and-drop subscribers in the editor.

Use when: Designer-configurable hooks (button clicks, trigger zones). Skip when: Pure code-side events (use Action — UnityEvent is heavier).

Lambda Expression

(x) => x * 2 — an anonymous function used inline.

Use when: Short, one-off callbacks. Skip when: The logic is non-trivial or used in multiple places (extract a method).

Real Project Patterns

Decoupling Systems with Events

// In PlayerHealth
public event Action<int> OnHpChanged;
public event Action OnDeath;

// In HpBarUI
void OnEnable()  => playerHealth.OnHpChanged += UpdateBar;
void OnDisable() => playerHealth.OnHpChanged -= UpdateBar;

UI doesn't reference gameplay logic. Gameplay doesn't reference UI. Either can be removed and the other still compiles.

Strategy Injection

public class EnemySpawner {
    public Func<Vector3> PickSpawnPoint;
    public Func<EnemyType> PickEnemyType;
    
    void Spawn() => Instantiate(PickEnemyType(), PickSpawnPoint(), Quaternion.identity);
}

Same spawner. Different missions configure different functions. Zero subclassing.

Async-Style Callbacks

public void LoadLevel(int id, Action onLoaded, Action<float> onProgress) {
    StartCoroutine(LoadCoroutine(id, onLoaded, onProgress));
}

Caller doesn't wait. UI shows progress. Game starts when loaded. All without async/await if your codebase is older.

Command Pattern Lite

Queue<Action> commandQueue = new();

void OnPlayerInput(Action cmd) => commandQueue.Enqueue(cmd);
void Update() {
    while (commandQueue.Count > 0) commandQueue.Dequeue()();
}

Defer execution, replay later, undo with the inverse — all from a queue of Action.

The Delegate Pitfalls

Memory Leaks (The Big One)

A subscriber holds a reference to its event source. If the source outlives the subscriber and you forget to unsubscribe, the subscriber stays alive forever.

Rule: Every += needs a -=. Use OnEnable/OnDisable or OnDestroy in Unity. Always.

Null Reference Exception on Invoke

If no one subscribed, the delegate is null. Calling null.Invoke() crashes.

Rule: Always ?.Invoke(...) or null-check.

Stale Closures

A lambda that captures a variable can hold the wrong value if the variable changes after the lambda is created. Loop variables especially.

Rule: Capture explicitly with a local copy if iterating.

Order of Subscribers Is Non-Deterministic

Don't write code that assumes Subscriber A runs before Subscriber B. If order matters, use an explicit pipeline, not events.

UnityEvent Is Slower Than Action

~10x slower in tight loops. Use Action for code-side events; reserve UnityEvent for designer-facing hooks.

How They Compose

The real power isn't algorithms or delegates alone — it's combining them.

  • A* pathfinder with a Func<Node, Node, float> heuristic injected → same algorithm, different cost models per game type.
  • Behavior Tree built from Func<bool> condition and Action action nodes → designers compose AI without coding.
  • Match-3 detector that fires Action<List<Tile>> onMatchFound → core algorithm, decoupled from VFX/sound/scoring layers.
  • Object pool with Action<T> onSpawned, onDespawned → reuse pattern + lifecycle hooks.

Algorithm decides what happens. Delegate decides who reacts to it. Most production code is exactly this pairing.

Bottom Line

Algorithms are the heart. Delegates are the nervous system.

A junior dev memorizes pattern names. A mid-level dev recognizes which algorithm fits. A senior dev wires the algorithm up so other systems can react without coupling — and that wiring is delegates.

If you only learn one thing this week: practice the Action/event/Func vocabulary in C#. It's the closest thing to a superpower in modern game code. Combined with the right algorithm choice, it's how shippable, maintainable game code actually gets built.

Chat