Logo Mibo

Services in Adaptive Programs

As your game grows, you will likely need services that are shared across your update and projection: things like Audio, Networking, or Save Data.

Instead of passing these individually or relying on global state, create a strongly typed environment record. The Elmish version of this guide covers the same pattern for that runtime; this page is the adaptive one, end to end.

The environment record

Initialize your services before you build the program, so they are ready before anything needs them and nothing has to be built twice.

// The "Env" pattern
type Env = {
    Audio: IAudioService
    Save: ISaveService
}

// 1. Create the environment independent of the program
let createEnv () = {
    Audio = AudioService.create()
    Save = SaveService.create()
}

Avoiding Circular References

A common pitfall is a service that needs the GameContext (to load sounds, say), which only exists once the host is running, so it feels like the service can't be built first.

On the adaptive side this has a clean answer: build the service in createEnv without the context, and give it an Init(ctx) method you call inside the program's setup, which runs before the first frame and already receives the context. No mutable "initialized later" fields, no ref cells.

Framework services are already registered for you; pull them from the context instead of building your own:

let assets = ctx.Context |> GameContext.getService<IAssets>

A note on async

Background work follows the same rules as everywhere in an adaptive game: never touch state containers from another thread. Queue the work with ctx.Intents.postTask or postAsync; when it completes, your ofSuccess callback applies the result on the game thread. The full contract (when to go off-thread, when to slice instead) is in Background Work.

One last habit worth keeping: frame counters, cost timers, and debug stats are plain mutable fields on your world, written from update. Don't increment a global from inside a derived value's computation; derived values are supposed to be pure (no side effects), and a hidden write in one is invisible to everything else.

Full program example

Here is the whole picture: an environment with two services, a small world, and the program from Adaptive Programs. The environment is created first, at the top; init, update and the program are named and applied to it:

open System.Numerics
open Mibo.Adaptive
open Mibo.Elmish

type Gem = { Pos: Vector2 }

type World = {
    Gems: cmap<int, Gem>
    Score: cval<int>
}

/// Everything the renderer needs, packed once per step.
type Frame = {
    Gems: System.Collections.Generic.IReadOnlyDictionary<int, Gem>
    Score: int
}

// 1. The environment, built before the program
let env = createEnv()

let world = { Gems = CMap.empty; Score = CVal.create 0 }

// Scratch buffer, created once and reused every step; the posted
// intent below clears it after draining, never during update
let taken = ResizeArray<int>()

let update (world: World) (ctx: AdaptiveContext) (gameTime: GameTime) =
    let dt = float32 gameTime.ElapsedGameTime.TotalSeconds

    for KeyValue(id, gem) in world.Gems |> AMap.getValue do
        let moved = { gem with Pos = gem.Pos + Vector2(dt, 0f) }
        world.Gems |> CMap.addOrUpdate id moved
        if moved.Pos.X > 10f then taken.Add id

    // Removals during the loop would invalidate the enumeration:
    // post them; the queue runs the work after update, before the frame is forced
    let takeGems () =
        world.Score.UpdateTo((world.Score |> AVal.getValue) + taken.Count) |> ignore

        for id in taken do
            world.Gems |> CMap.remove id
            // Synchronous service call: one-shot sounds are cheap
            env.Audio.PlayPickup()

        taken.Clear()

    if taken.Count > 0 then ctx.Intents.post takeGems

    // Autosave every ten pickups, off the game thread
    let score = world.Score |> AVal.getValue

    if score > 0 && score % 10 = 0 then
        let snapshot = score

        let saveScore () = env.Save.SaveScoreAsync(snapshot)
        let saved () = ()
        let saveFailed (ex: exn) = eprintfn $"autosave failed: {ex.Message}"

        ctx.Intents.postTask(saveScore, ofSuccess = saved, ofError = saveFailed)

/// The projection: `frame world` packs the `Frame`;
/// the runner forces it once per step.
let frame (world: World) () : Frame =
    { Gems = world.Gems |> AMap.getValue
      Score = world.Score |> AVal.getValue }

let init (world: World) (ctx: AdaptiveFrameContext) : AdaptiveInit<Frame> =
    // Services that need the game context (asset caches, audio devices)
    // initialize here: once, before the first frame
    env.Audio.Init(ctx.Context)
    AdaptiveInit.ofFrameBuilder (frame world)

// your drawing code: turns a Frame into drawing commands
// (see rendering.html)
let draw frameBuffer buffer = ...

let createRenderer () = Renderer2D.create draw

let program =
    AdaptiveProgram.mkProgram (init world) (update world)
    |> AdaptiveProgram.withConfig (GameConfig.withTitle "Gems")
    |> AdaptiveProgram.withRenderer createRenderer

[<EntryPoint>]
let main _ =
    AdaptiveRaylibGame<Frame>(program).Run()
    0
type Env = { Audio: obj Save: obj }
val createEnv: unit -> Env
val assets: obj
namespace System
namespace System.Numerics
type Gem = { Pos: Vector2 }
Multiple items
type Vector2 = new: value: float32 -> unit + 2 overloads member CopyTo: array: float32 array -> unit + 2 overloads member Equals: other: Vector2 -> bool + 2 overloads member GetHashCode: unit -> int member Length: unit -> float32 member LengthSquared: unit -> float32 member ToString: unit -> string + 2 overloads member TryCopyTo: destination: Span<float32> -> bool static member (&&&) : left: Vector2 * right: Vector2 -> Vector2 static member ( * ) : left: Vector2 * right: Vector2 -> Vector2 + 2 overloads ...
<summary>Represents a vector with two single-precision floating-point values.</summary>

--------------------
Vector2 ()
Vector2(value: float32) : Vector2
Vector2(values: System.ReadOnlySpan<float32>) : Vector2
Vector2(x: float32, y: float32) : Vector2
type World = { Gems: obj Score: obj }
Multiple items
val int: value: 'T -> int (requires member op_Explicit)

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

--------------------
type int<'Measure> = int
type Frame = { Gems: IReadOnlyDictionary<int,Gem> Score: int }
 Everything the renderer needs, packed once per step.
namespace System.Collections
namespace System.Collections.Generic
type IReadOnlyDictionary<'TKey,'TValue> = inherit seq<KeyValuePair<'TKey,'TValue>> inherit IEnumerable inherit IReadOnlyCollection<KeyValuePair<'TKey,'TValue>> override ContainsKey: key: 'TKey -> bool override TryGetValue: key: 'TKey * value: byref<'TValue> -> bool member Item: 'TValue member Keys: 'TKey seq member Values: 'TValue seq
<summary>Represents a generic read-only collection of key/value pairs.</summary>
<typeparam name="TKey">The type of keys in the read-only dictionary.</typeparam>
<typeparam name="TValue">The type of values in the read-only dictionary.</typeparam>
val env: Env
val world: Frame
val taken: ResizeArray<int>
type ResizeArray<'T> = System.Collections.Generic.List<'T>
val update: world: World -> ctx: 'a -> gameTime: 'b -> unit
val world: World
val ctx: 'a
val gameTime: 'b
val dt: float32
Multiple items
val float32: value: 'T -> float32 (requires member op_Explicit)

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

--------------------
type float32<'Measure> = float32
active recognizer KeyValue: System.Collections.Generic.KeyValuePair<'Key,'Value> -> 'Key * 'Value
val id: int
val gem: Gem
World.Gems: obj
val moved: Gem
Gem.Pos: Vector2
field Vector2.X: float32
<summary>The X component of the vector.</summary>
System.Collections.Generic.List.Add(item: int) : unit
val takeGems: unit -> unit
World.Score: obj
property System.Collections.Generic.List.Count: int with get
val ignore: value: 'T -> unit
Env.Audio: obj
System.Collections.Generic.List.Clear() : unit
val score: int
val snapshot: int
val saveScore: unit -> 'c
Env.Save: obj
val saved: unit -> unit
val saveFailed: ex: exn -> unit
val ex: exn
type exn = System.Exception
val eprintfn: format: Printf.TextWriterFormat<'T> -> 'T
property System.Exception.Message: string with get
val frame: world: World -> unit -> Frame
 The projection: `frame world` packs the `Frame`;
 the runner forces it once per step.
val init: world: World -> ctx: 'a -> 'b
val draw: frameBuffer: 'a -> buffer: 'b -> 'c
val frameBuffer: 'a
val buffer: 'b
val createRenderer: unit -> 'a
val program: obj
Multiple items
type EntryPointAttribute = inherit Attribute new: unit -> EntryPointAttribute

--------------------
new: unit -> EntryPointAttribute
val main: string array -> int

Type something to start searching.