Table of Contents

Loading content your way: texture and font factories

Every texture and font the content manager loads is made by a factory. A factory takes a small parameter object that says what to load, and returns the texture or font. The engine registers one factory for each kind of load, and a game can replace one of them or add a kind of its own. The content manager does not change either way: it asks for a texture or font by its parameters, and the factory that is registered for those parameters makes it.

How loading works

Content describes the calls a game makes. Underneath, GetTexture("sprites/ship.png") does this:

  1. It builds a LoadTextureByFileName with the resolved path and the colour key, if there is one.
  2. It asks ITextureFactoryProvider for a texture of that pair: ITexture made from LoadTextureByFileName.
  3. The provider looks up ITextureFactory<ITexture, LoadTextureByFileName> in the service container and calls CreateTexture on it.
  4. The content manager caches the result under the file it resolves to.

The lookup is by the exact closed generic type. The texture type and the parameter type together pick the factory, so one class can serve several pairs. The engine's TextureFactory serves five of them.

Fonts work the same way. GetBMFont and GetTrueTypeFont build a font parameter object and call IFontFactoryProvider, which resolves IFontFactory<TFontParameters>. A font's parameters implement IFontParameters, and a texture's implement ITextureParameters. Both are marker interfaces with no members.

The engine registers its factories in its own setup, before the game's Configure runs. Both providers are singletons, and so is the content manager. A game can resolve them by injection, as any other service. See Services and dependency injection.

The default factories

The engine's own factories are internal. The parameter types they take are public, so a game can use them directly, as shown in Using the providers directly below.

Textures in BSGameLib.Game

Parameters Creates Used by
LoadTextureByFileName(FileName, ColorKey) An ITexture from an image file. GetTexture
LoadTextureByBytes(Bytes, ColorKey) An ITexture from an encoded image in memory. Only the providers
LoadTextureByColors(Colors, Width, Height, ColorKey) An ITexture from an array of pixels. CreateTextureFromColorArray
LoadTextureByRenderTarget(Target) An ITexture from the pixels of a render target. Offscreen rendering
LoadTextureFromStream(Stream) An ITexture decoded from a stream. GetTextureFromStream
LoadTextureRegionByTextureWithRegions(Texture, Regions) An IRegionTextureAtlas with named regions over a texture. GetRegionTextureAtlas, GetRegionAtlas

Fonts in BSGameLib.Game

Parameters Creates Used by
LoadBMFontFromFile(FileName) An IFont from a BMFont .fnt file and its page images. GetBMFont
LoadBMFontFromStream(FileName, Stream, PageFolder) An IFont from a BMFont read from a stream. Its pages are read from PageFolder, or from the working directory when it is null. GetBMFontFromStream
LoadBMFontFromResource(AssemblyName, ResourceName, ImageResourceNames) An IFont from a BMFont embedded in an assembly. BSGameLib.UI's built-in fonts
LoadTrueTypeFontFromFile(FileName, Size, Bold, Italic) An IFont rasterised from a TrueType font at Size, emboldened when Bold and slanted when Italic. GetTrueTypeFont
LoadTrueTypeFontFromStream(FileName, Stream, Size, Bold, Italic) An IFont rasterised from a TrueType font read from a stream, emboldened and slanted the same way. GetTrueTypeFontFromStream

Bold and Italic are faked from the one face in the file or stream. Bold emboldens the glyphs and widens each advance by about a pixel per 16 pixels of size, so neighbours do not run together. Italic slants the glyphs to the right and keeps the upright advance, so the spacing of an italic line matches the plain one. The slant is given room in the glyph's atlas region, so no ink is clipped.

Textures and fonts in BSGameLib.UI

These are registered by AddUI(), so a game that uses the UI calls it in Configure.

Parameters Creates
LoadNinePatchParameters(Texture, Width, Height) An ITexture of the given size, drawn from a nine-patch template.
LoadHorizontalThreePatchParameters(Texture, Width, Height) An ITexture of the given width and height, drawn from a three-part horizontal template.

The UI also adds two fonts, Voltaire and Boogaloo, through factories that the game does not call. Name them as the font of a control, for example Font="Boogaloo". The UI looks each name up itself.

Replacing a default

A registration made in Configure replaces the engine's registration for the same pair. The container resolves the last registration made for a service, and the game's Configure runs after the engine's own. Register with AddSingleton, as the engine does. TryAddSingleton does nothing when a factory for the pair already exists, which the engine always provides.

This example loads textures from a zip file, and falls back to disk for any file the archive does not hold. It replaces the factory for ITexture from LoadTextureByFileName, so every GetTexture call on a plain file name goes through it:

using System.IO.Compression;
using BSGameLib.Game.Drawing;
using BSGameLib.Game.Textures;

public sealed class ZipTextureFactory : ITextureFactory<ITexture, LoadTextureByFileName>
{
  private readonly ITextureLoader _loader;
  private readonly string _archivePath;
  private readonly string _contentRoot;

  public ZipTextureFactory(ITextureLoader loader, string archivePath, string contentRoot)
  {
    _loader = loader;
    _archivePath = archivePath;
    _contentRoot = contentRoot;
  }

  public ITexture CreateTexture(LoadTextureByFileName parameters)
  {
    var entryName = Path.GetRelativePath(_contentRoot, parameters.FileName).Replace('\\', '/');

    if (File.Exists(_archivePath))
    {
      using var archive = ZipFile.OpenRead(_archivePath);
      var entry = archive.GetEntry(entryName);
      if (entry != null)
      {
        // A zip entry's stream cannot seek, and the loader needs to, so copy it first.
        using var entryStream = entry.Open();
        using var memory = new MemoryStream();
        entryStream.CopyTo(memory);
        memory.Position = 0;
        return _loader.Load(memory, parameters.ColorKey);
      }
    }

    return _loader.Load(parameters.FileName, parameters.ColorKey);
  }
}

The game registers it in Configure:

using BSGameLib.Game.Textures;
using Microsoft.Extensions.DependencyInjection;

public sealed class PackedGame : BSGameLib.Game.Game
{
  protected override void Configure(IServiceCollection collection)
  {
    var root = Path.Combine(AppContext.BaseDirectory, "Content");
    var archive = Path.Combine(AppContext.BaseDirectory, "content.zip");

    collection.AddSingleton<ITextureFactory<ITexture, LoadTextureByFileName>>(sp =>
      new ZipTextureFactory(sp.GetRequiredService<ITextureLoader>(), archive, root));
  }
}

The content manager passes the factory the full path of the file, under the root or an overlay. The factory turns that path back into the name the file has in the archive. Entry names are matched exactly, so the case in the archive must match the case the game asks for.

The archive is opened for each texture, which keeps the example short. A game that loads many textures should open it once and keep it. The game disposes its service container when it ends, after the content manager and before the window and renderer are disposed, so a factory that is IDisposable is disposed then. A factory that holds a file open can close it there.

The replacement does not cover the other pairs. GetTextureFromStream still uses the engine's LoadTextureFromStream factory, and it stays that way unless the game replaces that pair too.

Fonts

A font factory can be replaced in the same way, but the result is not much use. The only font that DrawText draws is the engine's internal BMFont, and a game has no way to make one except through the factories it would be replacing. To load fonts from a new source, add a parameter type that delegates to the engine's factory, as in the next section.

Adding a kind of texture

A game can add its own parameter type and factory. Here a tile is named in the game's code and loaded from a file by the name it maps to. The steps are the same for any kind of load.

First, define the parameter type. It is a record struct that implements ITextureParameters:

using BSGameLib.Game.Textures;

public readonly record struct LoadTileByName(string Name) : ITextureParameters;

Then write the factory. It returns an engine texture, and it can take any service it needs. This one takes IContentManager, so the tile is cached like any other texture:

using BSGameLib.Game;
using BSGameLib.Game.Textures;

public sealed class TileFactory : ITextureFactory<ITexture, LoadTileByName>
{
  private static readonly Dictionary<string, string> Files = new()
  {
    ["grass"] = "tiles/grass.png",
    ["water"] = "tiles/water.png",
  };

  private readonly IContentManager _content;

  public TileFactory(IContentManager content)
  {
    _content = content;
  }

  public ITexture CreateTexture(LoadTileByName parameters) => _content.GetTexture(Files[parameters.Name]);
}

Register it in Configure with the other factories:

collection.AddSingleton<ITextureFactory<ITexture, LoadTileByName>, TileFactory>();

Then call it through ITextureFactoryProvider:

using BSGameLib.Game.Textures;

public sealed class TileUser
{
  private readonly ITextureFactoryProvider _textures;

  public TileUser(ITextureFactoryProvider textures)
  {
    _textures = textures;
  }

  public ITexture Grass() => _textures.CreateTexture<ITexture, LoadTileByName>(new LoadTileByName("grass"));
}

IContentManager has no method for a parameter type of the game's own, so the provider is the way in. If nothing is registered for the pair, CreateTexture throws an InvalidOperationException.

The texture type can only be an engine texture. ITexture has members that are internal to the engine, so a game cannot implement it, and a factory returns ITexture or one of the engine's texture interfaces, such as IRegionTextureAtlas. The provider does not cache what a factory makes. A factory that wants caching must do it itself, as this one does through GetTexture.

Adding a kind of font

A font parameter type is the same shape, with IFontParameters in place of ITextureParameters. This one picks a BMFont file from a name, and hands the loading to the engine's factory for files:

using BSGameLib.Game.Fonts;

public readonly record struct LoadMenuFont(string Name) : IFontParameters;

public sealed class MenuFontFactory : IFontFactory<LoadMenuFont>
{
  private static readonly Dictionary<string, string> Files = new()
  {
    ["title"] = "fonts/title.fnt",
    ["body"] = "fonts/body.fnt",
  };

  private readonly IFontFactoryProvider _fonts;

  public MenuFontFactory(IFontFactoryProvider fonts)
  {
    _fonts = fonts;
  }

  public IFont Create(LoadMenuFont parameters)
  {
    var path = Path.Combine(AppContext.BaseDirectory, "Content", Files[parameters.Name]);
    return _fonts.CreateFont(new LoadBMFontFromFile(path));
  }
}

Register it in Configure, and call it through IFontFactoryProvider:

collection.AddSingleton<IFontFactory<LoadMenuFont>, MenuFontFactory>();
using BSGameLib.Game.Fonts;

public sealed class MenuText
{
  private readonly IFontFactoryProvider _fonts;

  public MenuText(IFontFactoryProvider fonts)
  {
    _fonts = fonts;
  }

  public IFont Title() => _fonts.CreateFont(new LoadMenuFont("title"));
}

A font made this way is not cached, so the game keeps the IFont it gets and disposes it when it is finished with. Fonts loaded through ContentManager are cached and disposed by it.

Using the providers directly

Loading through the providers is for content that the content manager does not load by file name. A texture made from encoded bytes is one example:

using BSGameLib.Game.Drawing;
using BSGameLib.Game.Textures;

public sealed class Portraits
{
  private readonly ITextureFactoryProvider _textures;
  private readonly ITextureLoader _loader;

  public Portraits(ITextureFactoryProvider textures, ITextureLoader loader)
  {
    _textures = textures;
    _loader = loader;
  }

  public ITexture FromFile(string path) =>
    _textures.CreateTexture<ITexture, LoadTextureByFileName>(new LoadTextureByFileName(path));

  public ITexture FromBytes(byte[] png) =>
    _textures.CreateTexture<ITexture, LoadTextureByBytes>(new LoadTextureByBytes(png));

  public ITexture FromStream(Stream stream) => _loader.Load(stream);

  public ITexture Blank(int width, int height) =>
    _loader.CreateFromColorArray(Enumerable.Repeat(Color.Transparent, width * height).ToArray(), width, height);
}

FromFile takes the path as given. Unlike GetTexture, it does not look under the root or in the overlays, and it does not cache.

The texture loader

ITextureLoader is the step under every texture factory. It reads an image and uploads it, and does nothing else: no cache, no root path and no overlays. Its members are:

  • Load(string fileName) and Load(string fileName, Color? colorKey) open the file at the path given.
  • Load(Stream stream) and Load(Stream stream, Color? colorKey) decode an image from a stream. The stream must be seekable, because the loader reads the file's header and then seeks back to the start.
  • CreateFromColorArray(Color[] colors, int width, int height, Color? colorKey = null) makes a texture from pixels.

A colour key makes every pixel exactly equal to it fully transparent, as it does for GetTexture.

The loader is registered as a singleton, so a game can take it by injection. The textures it returns are not cached or tracked. Dispose a texture yourself when you have finished with it, if it is IDisposable, as the engine's textures are.