Assets (loading + caching)
Mibo exposes a per-game asset service (IAssets) that wraps MonoGame’s ContentManager plus a simple cache.
What it provides:
- no module-level global caches (safe with multiple game instances/tests)
- fast repeated loads (
Texture,Model,Effect, etc are cached) - custom assets (shaders, JSON config, decoded content)
Enabling the service
Add it in your composition root:
Program.mkProgram init update
|> Program.withAssets
Then use the helpers from Mibo.Elmish.Assets anywhere you have a GameContext:
let init (ctx: GameContext): struct(Model * Cmd<Msg>) =
let player = Assets.texture "sprites/player" ctx
let font = Assets.font "fonts/ui" ctx
let shader = Assets.effect "Effects/Grid" ctx
{ PlayerTex = player; Font = font; Shader = shader }, Cmd.none
Custom assets (GPU-backed or not)
You can cache custom values (GPU-backed or not):
let outlineFx =
Assets.getOrCreate "OutlineEffect" (fun gd -> new Effect(gd, bytecode)) ctx
JSON helpers
Mibo includes JSON helpers via JDeck:
Assets.fromJson path decoderAssets.fromJsonCache path decoder ctx
These read JSON files from disk and optionally cache them for the game lifetime.
If you’re writing your own decoders, see the JDeck docs:
Building your own “store” on top of IAssets
IAssets is a convenient place to centralize loading + reuse.
For game-specific data sets (skills, items, quests, dialogue, etc), it can be useful to wrap IAssets in a specialized store that:
- owns decoding/validation
- chooses a backend (JSON, SQLite, embedded resources, network)
- can decide whether to cache (and how)
Example: SkillStore (JSON)
open Mibo.Elmish
open JDeck
type SkillId = SkillId of string
type Skill = {
Id: SkillId
Name: string
CooldownSeconds: float32
}
module SkillDecoders =
let skillDecoder : JDeck.Decoder<Skill> =
fun json -> JDeck.decode {
let! id = json |> Required.Property.get("id", Required.string)
let! name = json |> Required.Property.get("name", Required.string)
let! cd = json |> Required.Property.get("cooldownSeconds", Required.float32)
return {
Id = SkillId id
Name = name
CooldownSeconds = cd
}
}
type SkillStore(assets: IAssets) =
/// Load a skill list (no caching). Useful if you want to manage lifetime yourself.
member _.LoadAll(path: string) : Skill list =
Assets.fromJson path (Required.list SkillDecoders.skillDecoder)
/// Load and cache (game-lifetime). Useful for static data.
member _.LoadAllCached(path: string, ctx: GameContext) : Skill list =
Assets.fromJsonCache path (Required.list SkillDecoders.skillDecoder) ctx
/// A typed cache for an indexed view.
member _.IndexByIdCached(path: string, ctx: GameContext) : Map<SkillId, Skill> =
Assets.getOrCreate ("SkillStore/IndexById/" + path)
(fun _gd ->
let skills = Assets.fromJson path (Required.list SkillDecoders.skillDecoder)
skills |> List.map (fun s -> s.Id, s) |> Map.ofList)
ctx
You can create the store from context:
let store = SkillStore(Assets.getService ctx)
let skillsById = store.IndexByIdCached("Content/skills.json", ctx)
Example: SkillStore (SQLite)
If your source of truth is a database, the same idea applies: the store is where you choose I/O strategy and caching.
type SkillStoreDb(conn: System.Data.IDbConnection) =
member _.GetById(id: SkillId) : Skill option =
// query, map rows -> Skill
None
In this case you typically keep caching inside the store (LRU, size cap, explicit invalidation) instead of using IAssets as a forever-cache.
Cache growth and lifetime
The built-in IAssets caches are designed for game-lifetime reuse:
- the cache grows as you load more keys
- there is no built-in eviction policy
If memory usage is a concern, prefer one of these patterns:
- Use non-cached loads (
Assets.fromJson) and manage lifetime in your own store. - Build a store with an explicit cap/eviction strategy (LRU) and keep only hot entries.
- Structure data so it loads in “chunks” (per level/biome), and keep chunk lifetime separate from the global
IAssetscache.
Lifetime and disposal
- Content pipeline assets (
ContentManager.Load) are managed by MonoGame. - “Custom” assets you store via
Create/GetOrCreateare kept in a dictionary. IAssets.Dispose()will dispose any cached values that implementIDisposable.
If you store GPU resources (Effects, RenderTargets, etc) in the cache, prefer GetOrCreate so they’re created once per game.
union case SkillId.SkillId: string -> SkillId
--------------------
type SkillId = | SkillId of string
val string: value: 'T -> string
--------------------
type string = System.String
val float32: value: 'T -> float32 (requires member op_Explicit)
--------------------
type float32 = System.Single
--------------------
type float32<'Measure> = float32
type SkillStore = new: assets: obj -> SkillStore member IndexByIdCached: path: string * ctx: 'a -> Map<SkillId,Skill> member LoadAll: path: string -> Skill list member LoadAllCached: path: string * ctx: 'b -> Skill list
--------------------
new: assets: obj -> SkillStore
module Map from Microsoft.FSharp.Collections
--------------------
type Map<'Key,'Value (requires comparison)> = interface IReadOnlyDictionary<'Key,'Value> interface IReadOnlyCollection<KeyValuePair<'Key,'Value>> interface IEnumerable interface IStructuralEquatable interface IComparable interface IEnumerable<KeyValuePair<'Key,'Value>> interface ICollection<KeyValuePair<'Key,'Value>> interface IDictionary<'Key,'Value> new: elements: ('Key * 'Value) seq -> Map<'Key,'Value> member Add: key: 'Key * value: 'Value -> Map<'Key,'Value> ...
--------------------
new: elements: ('Key * 'Value) seq -> Map<'Key,'Value>
module List from Microsoft.FSharp.Collections
--------------------
type List<'T> = | op_Nil | op_ColonColon of Head: 'T * Tail: 'T list interface IReadOnlyList<'T> interface IReadOnlyCollection<'T> interface IEnumerable interface IEnumerable<'T> member GetReverseIndex: rank: int * offset: int -> int member GetSlice: startIndex: int option * endIndex: int option -> 'T list static member Cons: head: 'T * tail: 'T list -> 'T list member Head: 'T with get member IsEmpty: bool with get member Item: index: int -> 'T with get ...
A typed cache for an indexed view.
type SkillStoreDb = new: conn: IDbConnection -> SkillStoreDb member GetById: id: SkillId -> Skill option
--------------------
new: conn: System.Data.IDbConnection -> SkillStoreDb
<summary>Represents an open connection to a data source, and is implemented by .NET data providers that access relational databases.</summary>
Mibo