Haskell World logo Haskell WorldWrite code, build worlds
Games

How Online Game Logic Maps to Functional Programming Patterns

Game logic and functional programming have a relationship that is tighter than most developers initially appreciate.

How online game logic maps to functional programming patterns

Game logic and functional programming have a relationship that is tighter than most developers initially appreciate. Many of the problems that game logic needs to solve - representing state, handling events, combining behaviors, managing time-varying values - have clean solutions in the functional programming toolkit. Working through the connection between game problems and functional solutions reveals both why functional patterns are useful in games and how they can be applied in practice.

Game state as an algebraic data type

Every game has a notion of game state - the complete description of the game at a point in time that determines what will happen next given any input. Representing this state well is foundational to everything else in the game's code. In functional programming, the natural representation is an algebraic data type: a type that is either a product type (a record holding multiple fields) or a sum type (a type with multiple distinct variants, only one of which is active at any time).

Sum types are particularly useful for representing states that are fundamentally different from each other. A game that has a main menu state, a gameplay state, and a game-over state has three variants with different data requirements. The main menu state might carry only the current menu selection. The gameplay state carries all the entity positions, player stats, and world state. The game-over state carries the final score and any replay data. A sum type makes it impossible to accidentally access gameplay state data when the game is in the main menu - the type system enforces that you check which variant you are in before accessing variant-specific data.

Haskell's pattern matching on sum types is exhaustive by default: if you add a new variant to a sum type, every match expression that does not cover the new variant produces a compiler warning. This is invaluable during game development, when new states are frequently added. The compiler finds every place that needs to be updated when a new state is introduced, preventing the silent bugs that come from missing a case in an if-else chain.

Game updates as pure functions

The central operation in game logic is the update: given the current game state and the current inputs, produce the next game state. Expressed as a function, this is:

update :: GameState -> Inputs -> GameState

The value of making this function pure - deterministic, no side effects - is substantial. The same inputs always produce the same next state. You can test this function in isolation without setting up a full game environment. You can record a sequence of inputs and replay them deterministically to reproduce any game situation. You can run the update function headlessly, without graphics or audio, at high speed to simulate many frames for testing or AI training.

Side effects - playing sounds, updating the display, sending network messages - are separated from the update function. The update function produces a list of effects along with the new state, and a separate handler executes those effects. This is the IO monad pattern at the architectural level: pure computation separated from effectful execution. The game logic is pure and testable; the world-facing operations are explicit and isolated.

Entity behavior as function composition

In object-oriented game development, entity behavior is typically implemented through class hierarchies. Enemies inherit from Character, which inherits from Entity. Behaviors are virtual methods. The classic problems with this approach are familiar: deep hierarchies become fragile, multiple inheritance creates complexity, and adding a behavior to multiple unrelated entity types requires either duplicating code or reshaping the hierarchy.

The functional alternative is composition. A behavior is a function from entity state to entity state, possibly parameterized. An enemy that moves toward the player and fires when in range is the composition of a "move toward target" behavior and a "fire when in range" behavior. Composing behaviors means applying one after the other, or combining them with a merging strategy when they conflict.

This approach is the functional basis of entity-component systems, which have become the dominant pattern in game engine design. Components are data; systems are functions that operate on entities with specific component combinations. The composition happens through system scheduling rather than function composition, but the underlying idea is the same: behaviors are defined in terms of their inputs and outputs, and complex behaviors emerge from the combination of simple ones.

Time-varying values and functional reactive programming

Many game values vary over time: a character's position, an animation's progress, a health regeneration rate. In imperative code, these are typically mutable variables updated at each frame. In functional code, they can be modeled as functions of time, which eliminates the mutation and makes the temporal behavior explicit.

Functional Reactive Programming (FRP) provides a systematic framework for this. A behavior in FRP is a time-varying value: a function from time to a value. An event in FRP is a discrete occurrence at a specific time with an associated value. The FRP library provides combinators for transforming and combining behaviors and events, and a runtime that evaluates them as time advances.

The Yampa library in Haskell implements an FRP variant called Arrowized FRP, where signal functions - transformations from input signals to output signals - are first-class values that compose through arrow combinators. A game implemented with Yampa models each entity as a signal function and the game as a network of signal functions. The resulting code is declarative: it says what the values are at each time, not how to compute the transitions between values.

Collision detection and its functional structure

Collision detection is one of the computationally intensive parts of game logic, and its structure maps well to functional programming. The naive algorithm - check every pair of entities for collision - is O(n^2) and unsuitable for games with many entities. Spatial partitioning data structures - quadtrees, octrees, bounding volume hierarchies - accelerate collision detection by restricting which pairs need to be checked.

Building a spatial partition from a list of entities is a pure function: given a list of entity positions and bounding volumes, produce the partition data structure. Querying the partition for potential collisions is a pure function: given the partition and a query region, produce a list of entities that might intersect. The actual collision test is a pure function: given two entity shapes and positions, produce a boolean or a collision manifold.

Pure collision detection functions are easy to test. Given a specific set of entity positions, the collision detection should return a specific set of collision pairs. You can write tests that exercise specific geometric configurations - two circles just touching, two axis-aligned boxes overlapping at a corner, a fast-moving sphere passing through a thin wall - and verify the results. The absence of state means that the test results are not affected by execution order or setup code.

AI decision making as computation

Game AI needs to decide what to do on each frame, given the current game state and the AI's own state. Behavior trees - the most widely-used AI architecture in games - are executable data structures: trees of conditions and actions that are evaluated top-down until a leaf action is selected. The evaluation of a behavior tree is a pure computation over the tree data structure and the game state.

Representing behavior trees as data enables tools: a visual editor that lets designers construct and modify trees without writing code. It enables testing: given a specific game state, evaluating the tree should produce a specific action, and this can be verified in a unit test. It enables debugging: recording the evaluation path through the tree for each frame produces a log that shows exactly why the AI made each decision.

The functional view of behavior trees as data structures that are evaluated by a pure interpreter is directly applicable to any game AI system where decisions are based on observable state. The interpreter is fixed; the data it interprets is what the designer configures. This separation is the same principle underlying any data-driven system, and it pays the same dividends: flexibility for designers, testability for developers, and debuggability for everyone.

The practical argument for functional game logic

The practical case for functional patterns in game logic is not ideological. It is grounded in the specific benefits that functional approaches provide for the specific problems that game logic presents. Determinism enables replays and reproducible tests. Pure functions enable isolated testing without engine infrastructure. Sum types enable exhaustive state handling. Composition enables building complex behaviors from verified simple parts.

None of these benefits require using a pure functional language. They are approaches that can be adopted incrementally in any language. A game logic layer that is isolated from rendering and audio code, that represents state explicitly as data, that passes state through functions rather than mutating global variables - this is the functional approach, available in C++, C#, JavaScript, or any language that supports first-class functions and value types. The patterns translate across languages, and the benefits travel with them.

SR
Sonja Reiter

Sonja Reiter contributed to three major open source projects and started writing documentation when she noticed most tooling guides were written by people who had never taught anyone to use them. She covers build tools, package ecosystems, and the invisible quality-of-life differences between development setups.

More posts by Sonja

More in Games