Recursive AI

Load game data from YAML in C#

onegame is a free, open-source game engine for .NET from Recursive AI. MIT or Apache-2.0, no editor required.

Load YAML balance data into an application type through onegame’s asset cache. Path-based loading is supported on Windows, Linux and macOS, not Android, iOS or the browser.

Terminal
dotnet add package \
  RecursiveAI.OneGame.Host

Requires .NET 10 (net10.0); .NET 9 and earlier aren’t supported.

data/ships.yaml:

YAML
name: Scout
gold: 25
C#
using OneGame.Host;
using OneYaml;
 
using var assets = new Assets();
var ships = assets.LoadData(
    "data/ships.yaml", GameYaml.Default.Ships);
Console.WriteLine(ships.Value.Name);
ships.Release();
 
public record Ships(string Name, int Gold);
[YamlSerializable(typeof(Ships))]
partial class GameYaml
    : YamlSerializerContext;

Prints:

Text
Scout

Unzip the example, then dotnet run.

The full program

C#
using OneGame.Host;
using OneYaml;
 
using var assets = new Assets();
var ships = assets.LoadData(
    "data/ships.yaml", GameYaml.Strict.Ships);
Console.WriteLine(
    $"{ships.Value.Name}: {ships.Value.Gold} gold");
ships.Release();
 
public sealed record Ships(string Name, int Gold);
 
[YamlSerializable(typeof(Ships))]
internal sealed partial class GameYaml
    : YamlSerializerContext
{
    internal static GameYaml Strict { get; } =
        new(new YamlSerializerOptions
    {
        UnmappedMemberHandling =
            YamlUnmappedMemberHandling.Disallow
    });
}

dotnet run prints:

Text
Scout: 25 gold

The generated GameYaml context supplies the schema. The full program adds strict options to the snippet at the top. The strict options reject keys that are not members of Ships. Keep the returned handle while the game uses it, read through Value, and release each acquired reference when finished.

Missing files, invalid YAML and unknown keys throw AssetException; a failed initial load caches nothing.

Variations

Share a cached handle

C#
// Before ships.Release() in the program above:
var again = assets.LoadData("data/ships.yaml", GameYaml.Strict.Ships);
Console.WriteLine(ReferenceEquals(ships, again));
Console.WriteLine(ships.References);
again.Release();

Prints these additional lines:

Text
True
2

Animate a glTF character · Handle trigger events


The short answer

Use assets.LoadData(path, typeInfo) to load YAML into your game’s application type. Supply metadata generated by a OneYaml serializer context, keep the returned Asset<T> handle, and read its Value when using the data. Release the handle when that owner no longer needs it.

The standalone download creates an Assets service directly instead of starting a game host. When working with a GameInstance named game, use its existing game.Assets service so the host and development tools share the cache and reload state.

How do I deserialize YAML into a C# game-data type?

Declare a type that matches the file’s shape, mark a partial serializer context with YamlSerializable, and pass the generated type metadata to LoadData. The sample’s Ships record has a name and a gold amount. Its input uses name and gold; these key spellings matter with the generated metadata and strict options shown here.

OneYaml comes with RecursiveAI.OneGame.Host; no separate YAML package installation is needed. Building the project generates GameYaml.Strict.Ships metadata from the context declaration; it is not a handwritten parser or a method you must implement. The program sets UnmappedMemberHandling to Disallow, so a typo does not silently leave a property at a default value.

Paths are relative to content folders and use forward slashes. They may not be absolute and may not contain ... With the default file source, the game looks beside the program, in a macOS bundle’s Resources directory, or in the working directory. Keeping data/ships.yaml beside the project makes the example runnable from the unzipped folder.

A failed initial load throws AssetException and caches nothing. Its code distinguishes failures such as a missing file, bad path, excessive size or bad format. Treat a parse failure as a data error to fix rather than continuing with a partially populated balance table. The default YAML file size limit is 8 MiB.

How do I cache and reload YAML game data?

The asset cache shares a handle for repeated loads of the same path and counts references. Each successful load acquires a reference; each owner must call Release() once when finished. At zero references the asset leaves the cache. A handle is not IDisposable, so use Release, not a using statement around Asset<Ships>.

Fixed is a deterministic fixed-point number type; FVec3 is a three-dimensional vector of those numbers. The sample’s balance record needs only a string and an integer.

Keep the handle rather than copying its Value into a field forever. YAML reloads replace the decoded object. Reading through the handle when applying game rules lets an intentional data update take effect at the chosen boundary without leaving some consumers on an older object.

Development YAML changes are staged, not swapped immediately. The changed file is parsed and checked, then the handle becomes pending while Value still returns the old data. Apply it with assets.ApplyPendingData(), the scenario assets: apply-data step, or the automatic boundary at scenario start. Never apply pending data while a recording is running: changing simulation inputs mid-recording would undermine replay and drift checks.

A bad reload preserves the old content, sets Asset.Error, and raises a failed event. Fixing the file permits another reload. DataDigest identifies the loaded data files so a replay can record which inputs it used. Exported games do not watch files; file watching belongs to development tools. Path loading is also not yet available on Android, iOS or the browser, where content needs the documented embedded route instead. See the asset guide for handle ownership, staged reloads and platform coverage.

All onegame recipes