Assets (loading + caching)
Mibo provides a simple, functional API for loading and caching game assets through the Mibo.Elmish.Assets module. It wraps MonoGame's ContentManager with automatic caching so you never load the same texture twice.
Enabling the service
Add it in your composition root:
Program.mkProgram init update
|> Program.withAssets
Loading Standard Assets
Once enabled, use these functions anywhere you have a GameContext:
let init (ctx: GameContext): struct(Model * Cmd<Msg>) =
// Load and cache content pipeline assets
let player = Assets.texture "sprites/player" ctx
let font = Assets.font "fonts/ui" ctx
let bgm = Assets.sound "audio/background" ctx
let enemyModel = Assets.model "models/enemy" ctx
let shader = Assets.effect "Effects/Grid" ctx
{ PlayerTex = player
Font = font
Bgm = bgm
Enemy = enemyModel
Shader = shader }, Cmd.none
All these functions cache results automatically:
Function |
Returns |
Description |
|---|---|---|
|
|
2D image asset |
|
|
Bitmap font |
|
|
Audio effect |
|
|
3D model |
|
|
Shader/effect |
Custom Assets
For assets not loaded through the content pipeline, use getOrCreate:
<<<<<<< Updated upstream
// Create a custom shader once, reuse forever
let outlineFx =
Assets.getOrCreate "OutlineEffect" (fun gd -> new Effect(gd, bytecode)) ctx
// Create a render target
let rt =
Assets.getOrCreate "MainRenderTarget"
(fun gd -> new RenderTarget2D(gd, 1920, 1080)) ctx
Why getOrCreate? It's idempotent - safe to call multiple times, creates only once.
For assets that might not exist yet, use get:
match Assets.get<MyConfig> "config" ctx with
| ValueSome cfg -> cfg
| ValueNone -> loadDefaultConfig()
To force creation (overwriting any existing), use create:
// This replaces any existing "playerData" entry
Assets.create "playerData" (fun _ -> loadPlayerFromDisk()) ctx
Loading Non-Pipeline Files
### JSON with JDeck
// Create once, cache forever - preferred approach let outlineFx = Assets.getOrCreate "OutlineEffect" (fun gd -> new Effect(gd, bytecode)) ctx
// Check if already cached without creating
match Assets.get
// Force creation (overwrites existing - use with caution) let freshData = Assets.create "tempData" (fun _ -> calculateExpensive()) ctx
**Note:** `Assets.get` only retrieves custom assets created via `create` or `getOrCreate`. Content pipeline assets (textures, models, etc.) are loaded through `Assets.texture`, `Assets.model`, etc.
# JSON helpers
>>>>>>> Stashed changes
Mibo includes JSON helpers via [JDeck](https://github.com/AngelMunoz/JDeck):
```fsharp
// One-off load (no caching)
let config = Assets.fromJson "config.json" myDecoder
// Cached for game lifetime
let data = Assets.fromJsonCache "data/levels.json" levelDecoder ctx
For writing decoders, see the JDeck docs:
Custom File Loaders
Load any file type with custom logic:
// One-off load
let raw = Assets.fromCustom "data/save.dat" File.ReadAllBytes ctx
// Cached
let parsed =
Assets.fromCustomCache "data/items.csv"
(fun path -> parseCsvFile path) ctx
Building Data Stores
For game-specific datasets (skills, items, quests), wrap asset loading in a typed store:
type SkillId = SkillId of string
type Skill = {
Id: SkillId
Name: string
CooldownSeconds: float32
}
module SkillStore =
let loadAll (path: string) : Skill list =
// No caching - manage lifetime yourself
Assets.fromJson path SkillDecoders.list
let loadAllCached (path: string) (ctx: GameContext) : Skill list =
// Cached for game lifetime
Assets.fromJsonCache path SkillDecoders.list ctx
let indexById (path: string) (ctx: GameContext) : Map<SkillId, Skill> =
// Custom cached transformation
Assets.getOrCreate ("skills/index/" + path)
(fun _ ->
let skills = Assets.fromJson path SkillDecoders.list
skills |> List.map (fun s -> s.Id, s) |> Map.ofList)
ctx
Usage:
let skillsById = SkillStore.indexById "Content/skills.json" ctx
Cache Behavior
Automatic caching applies to:
- All standard assets (texture, font, sound, model, effect)
- Anything loaded via getOrCreate or *Cache variants
Manual lifetime management:
- Use Assets.fromJson or Assets.fromCustom for one-off loads
- Build your own store with LRU/eviction if memory is a concern
Clearing caches:
// Get the underlying service (advanced)
let service = Assets.getService ctx
// Clear all custom caches (standard assets remain)
service.Clear()
// Dispose custom assets and clear everything
service.Dispose()
Advanced: Direct IAssets Access
For advanced scenarios, access the underlying IAssets service:
// Safe retrieval
match Assets.tryGetService ctx with
| ValueSome svc -> // use service
| ValueNone -> // service not registered
// Throws if not registered
let svc = Assets.getService ctx
IAssets provides the same operations if you need to pass it around as a value:
let loadStuff (assets: IAssets) =
let tex = assets.Texture "sprite"
let fx = assets.GetOrCreate "fx" (fun gd -> ...)
...
Prefer the Assets.* functions - they're more ergonomic and require less plumbing.
Performance Notes
- First load reads from disk; subsequent loads return cached reference
- No built-in eviction - caches grow with unique keys loaded
- GPU resources (textures, effects, render targets) are created once via
getOrCreate Dispose()on the service cleans up custom assets implementingIDisposable
For large games, consider:
- Chunked loading (per level/biome) with separate cache scopes
- Custom stores with LRU eviction for dynamic content
- Non-cached loads for one-time data
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
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 ...
Mibo