Logo Mibo

The Adaptive Architecture

In the adaptive architecture your game is State, Projection, Update (SPU). The state lives in containers that know when they changed; the projections describe the values you want derived from those facts; the update advances the game by writing facts. The framework takes care of the rest: what to recompute, when, and how the renderer gets it.

Two bits of F# you will see in every example: |> feeds the value on its left into the function on its right, and $"Honey: {h}" plugs a value into a string.

open Mibo.Adaptive

// State: the facts you write
let honey = CVal.create 0
let bees = CMap.empty<int, Bee>

// Projections: values derived from the facts, never updated by hand
let beeCount = bees |> AMap.count        // read with AVal.getValue when packing
let honeyStatus h = $"Honey: {h}"
let status = honey |> AVal.map honeyStatus

Write to bees ten times between two reads and the projections recompute once, not ten times. Don't write at all and reads are free. That is the whole trick: you stop keeping derived values up to date, because the graph does it.

The frame projection

Once per step the runner forces the projection you registered, which packs everything the renderer needs into one value (your "frame") and hands it to your drawing code. The renderer never looks at the game state directly; it reads the frame. This keeps the two halves independent: you can reorganize your state without touching a single draw call, and vice versa.

The frame is forced after the update has run, so it always reflects this step's world. Anything the renderer should see (positions, health bars, score, UI state) goes in there. Adaptive Programs shows the full shape.

Is this for my game?

Reach for the adaptive architecture when your game is mostly a simulation: things move, counts change, the HUD follows the world. The bigger that gets, the more this pays off.

If your game is turn-based, menu-driven, or small enough that recomputing everything each frame doesn't show up in the profiler, the simpler Elmish architecture is a better fit. Both run on the same backends and share the same rendering; pick one per project and go.

Where to go next

val honey: obj
val bees: obj
Multiple items
val int: value: 'T -> int (requires member op_Explicit)

--------------------
type int = int32

--------------------
type int<'Measure> = int
val beeCount: obj
val honeyStatus: h: 'a -> string
val h: 'a
val status: obj

Type something to start searching.