Header menu logo Mibo.Raylib

Pre-computed Derived State

What and Why

Many game values depend on other values that change every frame — sky color depends on time, visibility depends on positions, health bars depend on hit points. The naive approach computes these in the view function. This couples logic to rendering, duplicates computation across systems, and makes testing impossible.

The pattern: compute derived values once per frame in a dedicated system. Store results in a lightweight model. Every other system — rendering, AI, UI — reads the pre-computed values without recalculating.

Use Cases

Day/night cycle

Time drives sky color, light direction, ambient intensity, and shadow parameters. A lighting system computes all of these from the time-of-day. The renderer reads them directly.

Animation state

Time drives bone matrices, sprite frames, and blend weights. An animation system computes poses from time. The renderer applies them to meshes.

AI perception

Positions drive visibility, threat level, and awareness. A perception system computes which enemies can see the player, which are flanking, which are distracted. The behavior tree reads these results.

Physics queries

Positions and velocities drive nearest enemy, line of sight, and predicted intercept points. A query system computes these. The AI and combat systems read them.

UI state

Game state drives health bar widths, cooldown timers, and resource counters. A UI state system computes display values from raw data. The HUD reads them without touching game logic.

Weather effects

Time and position drive wind direction, precipitation intensity, and fog density. A weather system computes these from game state. The renderer and physics system read them.

The Technique

Compute derived values in a dedicated system:

let lightingSystem (dt: float32) (model: GameModel) : struct (GameModel * Cmd<Msg>) =
  let time = model.TimeOfDay
  model.Lighting.SkyColor <- getSkyColor time
  model.Lighting.LightDirection <- getSunDirection time
  model.Lighting.AmbientIntensity <- getAmbientIntensity time
  struct (model, Cmd.none)

Store results in a lightweight model:

type LightingModel() =
  member val SkyColor = Color.Black with get, set
  member val LightDirection = Vector3.Zero with get, set
  member val AmbientIntensity = 0.0f with get, set

The view reads pre-computed values — zero computation:

let view (ctx: GameContext) (model: GameModel) (buffer: RenderBuffer3D) =
  let l = model.Lighting
  buffer
  |> Draw3D.beginCameraWith (Camera3D.render camera |> Camera3D.withClear l.SkyColor)
  |> Draw3D.setAmbientLight { Color = l.SkyColor; Intensity = l.AmbientIntensity }
  |> Draw3D.addDirectionalLight { Direction = l.LightDirection; ... }

Systems run in order, so derived systems run after their inputs:

System.start model
|> System.pipeMutable (dayNightSystem dt)    // clock first
|> System.pipeMutable (lightingSystem dt)    // compute from clock
|> System.finish id

Key Insight

Moving computation from the view to a system means: - The view stays simple — it just reads state. - Systems can be tested independently — no renderer needed. - Derived values are available to all systems, not just rendering. - The render path does minimal work.

The same derived value can feed multiple consumers. Lighting affects rendering, but also AI (visibility in dark areas) and gameplay (torch necessity). Pre-computing once means every consumer reads the same consistent value.

When to use

See also

val lightingSystem: dt: float32 -> model: 'a -> struct ('a * 'b)
val dt: float32
Multiple items
val float32: value: 'T -> float32 (requires member op_Explicit)

--------------------
type float32 = System.Single

--------------------
type float32<'Measure> = float32
val model: 'a
val time: obj
Multiple items
type LightingModel = new: unit -> LightingModel member AmbientIntensity: float32 with get, set member LightDirection: obj with get, set member SkyColor: obj with get, set

--------------------
new: unit -> LightingModel
val set: elements: 'T seq -> Set<'T> (requires comparison)
val view: ctx: 'a -> model: 'b -> buffer: 'c -> 'd
val ctx: 'a
val model: 'b
val buffer: 'c
val l: obj
namespace System
val id: x: 'T -> 'T

Type something to start searching.