Table of Contents

Asset Management

AssetManager loads typed assets through dependency-injected loaders and caches them by asset type and string key. Loading is independent of rendering, so game code works with Texture2D, SoundAsset, and FontAsset rather than file-format-specific APIs.

Built-in asset types

Asset type Loader Supported input
Texture2D TextureAssetLoader PNG, JPEG, BMP, and other formats supported by stb_image
SoundAsset SoundAssetLoader RIFF/WAVE PCM, mono or stereo, 8-bit or 16-bit
FontAsset FontAssetLoader TrueType and OpenType fonts supported by SkiaSharp

The built-in loaders are registered by AddAxolotl2D() and by both game-host registration methods.

Load from files

Load assets in Game.InitializeAsync. The game host awaits initialization before the window and first scene start:

public sealed class MyGame : Game
{
    private readonly AssetManager assets;

    public MyGame(IServiceProvider services, AssetManager assets) : base(services)
    {
        this.assets = assets;
    }

    protected override async Task InitializeAsync(
        CancellationToken cancellationToken = default)
    {
        await assets.LoadFileAsync<Texture2D>(
            "player", "Assets/player.png", cancellationToken);
        await assets.LoadFileAsync<SoundAsset>(
            "jump", "Assets/jump.wav", cancellationToken);
        await assets.LoadFileAsync<FontAsset>(
            "ui", "Assets/Inter-Regular.ttf", cancellationToken);
    }

    protected override void Cleanup() { }
}

LoadFileAsync<T>() owns and closes the file stream after the loader finishes.

Load from streams or resources

Use LoadAsync<T>() when another system supplies a stream. The caller retains ownership of that stream. Use LoadEmbeddedAsync<T>() for an assembly resource:

await assets.LoadEmbeddedAsync<Texture2D>(
    "logo",
    typeof(MyGame).Assembly,
    "MyGame.Assets.logo.png",
    cancellationToken);

Retrieve and unload

Texture2D player = assets.Get<Texture2D>("player");

if (assets.TryGet<SoundAsset>("jump", out var jump))
{
    audioPlayer.Play(jump!);
}

assets.Unload<Texture2D>("temporary-background");

Keys are separated by asset type, so a texture and sound can use the same key. Loading the same type-and-key pair again returns the first cached asset rather than replacing it. Get<T>() throws when no match exists; TryGet<T>() supports optional content.

Unloading disposes assets that implement IDisposable. Disposing AssetManager unloads every cached asset.

Add a custom asset loader

Implement IAssetLoader<TAsset> and register it with DI:

public sealed record Dialogue(IReadOnlyList<string> Lines);

public sealed class DialogueAssetLoader : IAssetLoader<Dialogue>
{
    public async ValueTask<Dialogue> LoadAsync(
        Stream stream,
        CancellationToken cancellationToken = default)
    {
        using var reader = new StreamReader(stream, leaveOpen: true);
        var text = await reader.ReadToEndAsync(cancellationToken);
        return new Dialogue(text.Split('\n'));
    }
}
services.AddSingleton<IAssetLoader<Dialogue>, DialogueAssetLoader>();

The loader can request its own dependencies through constructor injection. Keep loaders focused on decoding source data into an asset; let rendering and gameplay services decide how to use it.