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:
- It builds a
LoadTextureByFileNamewith the resolved path and the colour key, if there is one. - It asks
ITextureFactoryProviderfor a texture of that pair:ITexturemade fromLoadTextureByFileName. - The provider looks up
ITextureFactory<ITexture, LoadTextureByFileName>in the service container and callsCreateTextureon it. - 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)andLoad(string fileName, Color? colorKey)open the file at the path given.Load(Stream stream)andLoad(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.