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 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
val int: value: 'T -> int (requires member op_Explicit)
--------------------
type int = int32
--------------------
type int<'Measure> = int
Everything the renderer needs, packed once per step.
<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 float32: value: 'T -> float32 (requires member op_Explicit)
--------------------
type float32 = System.Single
--------------------
type float32<'Measure> = float32
<summary>The X component of the vector.</summary>
The projection: `frame world` packs the `Frame`;
the runner forces it once per step.
type EntryPointAttribute = inherit Attribute new: unit -> EntryPointAttribute
--------------------
new: unit -> EntryPointAttribute
Mibo