Install the onegame command from NuGet, create a game, run, test and export it, then write an entity with synced state, a command and a view, and test it with a scenario.
onegame is a C# game engine with no editor. A game is plain C# plus text assets, and the onegame command builds, runs, tests, captures, replays and exports it from a terminal. Every command and every line of output on this page comes from one run of the 1.1.2 packages from NuGet, on Linux x64 with the .NET 10 SDK and no engine checkout.
Install
You need the .NET 10 SDK. The onegame command is a .NET tool:
dotnet tool install -g RecursiveAI.OneGame.Cli --version 1.1.2You can invoke the tool using the following command: onegame Tool 'recursiveai.onegame.cli' (version '1.1.2') was successfully installed.
- The tool is framework-dependent. If .NET lives in your home folder (the
dotnet-installscript puts it in~/.dotnet) rather than in a system location, setDOTNET_ROOTto that folder, or the tool stops with “You must install .NET to run this application.” onegame helplists every command. The exit codes are the same for all of them: 0 success, 1 a check failed, 2 bad command line, 3 the build failed, 4 the game or tool could not run, 5 the option needs a Debug build.- The license is MIT or Apache-2.0, at your choice.
Create a game
onegame new orchard cd orchard
created /tmp/games/orchard cd /tmp/games/orchard onegame build onegame run --headless --frames 60 onegame test
The folder holds orchard.csproj, Program.cs, Game.cs, scenarios/click-button.yaml, a README.md and fonts/ with Noto Sans Regular and its license. The project file is the whole build setup:
<Project Sdk="RecursiveAI.OneGame.Sdk/1.1.2"> <PropertyGroup> <AssemblyName>Orchard</AssemblyName> <RootNamespace>Orchard</RootNamespace> </PropertyGroup> </Project>
The RecursiveAI.OneGame.Sdk package sets the target framework and the build rules (warnings are errors, nullable on, implicit usings off), references the engine libraries at its own version, adds the development tools to Debug builds and carries the export targets. Everything restores from nuget.org. Your first game walks through the scaffold’s Game.cs line by line.
Run it
onegame run --headless --frames 60 --json{"ok":true,"game":"orchard","headless":true,"frames":60,"tick":60,"hash":"3245c24a6e2d4459","width":1280,"height":720}run builds the game and runs it. --headless needs no window or display, and --frames 60 stops after 60 frames. hash is the hash of the whole world after the last tick: the same game, seed and inputs give the same hash every time. onegame run without --headless opens a window.
Test it
onegame testPASS click-button (30 ms, tick 2, hash 78c58ac33e3bb232) 1 passed, 0 failed
test runs every file in scenarios/ in a headless game, plus any C# test projects in the folder. With --json the result is one object, with each step and expectation and the line it came from:
{"ok":true,"scenarios":{"ok":true,"passed":1,"failed":0,"scenarios":[{"name":"click-button","source":"/tmp/games/orchard/scenarios/click-button.yaml","passed":true,"tick":2,"hash":"78c58ac33e3bb232","elapsedMs":30,"steps":[{"index":0,"kind":"expect","line":4,"ok":true},{"index":1,"kind":"click","line":6,"ok":true},{"index":2,"kind":"step","line":7,"ok":true},{"index":3,"kind":"click","line":8,"ok":true},{"index":4,"kind":"step","line":9,"ok":true}],"expectations":[{"index":0,"kind":"ui","line":5,"passed":true,"target":"#count","actual":"Count: 0"},{"index":0,"kind":"state","line":11,"passed":true,"target":"Counter.Count","expected":"2","actual":"2"},{"index":1,"kind":"ui","line":12,"passed":true,"target":"#count","actual":"Count: 2"}]}]},"unitTests":{"ran":false,"ok":true,"total":0,"passed":0,"failed":0,"skipped":0,"projects":[]}}Export it
onegame export --target linuxexported orchard 1.0.0 for linux-x64 (nativeaot): 5 files, 7408 KiB folder: /tmp/games/orchard/export/orchard-1.0.0-linux-x64 archive: /tmp/games/orchard/export/orchard-1.0.0-linux-x64.tar.gz (3583 KiB) warning glibc_too_new: Orchard needs glibc 2.38, newer than 2.31 in Steam's Linux runtime (and so on the Steam Deck). Build on an older system, or inside the Steam Linux Runtime SDK: see docs/exporting.md.
The folder holds the NativeAOT program Orchard, run.sh, fonts/ and THIRD-PARTY-NOTICES.txt. The development tools are left out. The exported program gives the same hash as onegame run:
export/orchard-1.0.0-linux-x64/run.sh --headless --frames 60 --json{"ok":true,"game":"orchard","headless":true,"frames":60,"tick":60,"hash":"3245c24a6e2d4459","width":1280,"height":720}- The warning matters only for Steam: the export was built on a system with a newer glibc than the Steam Linux runtime. See Ship a game for Steam and the Steam Deck.
--target winand--target osxalso work from the packages. NativeAOT needs a host of the same OS, so from Linux they make a self-contained build instead and say so in a warning. See Export for Linux, Windows and macOS.- For a NativeAOT build the repository’s docs list the platform’s C++ toolchain:
clangandzlib1g-devon Linux, the Visual Studio C++ tools on Windows. - Web, Android and iOS exports need an engine checkout at 1.1.2. A game made from the packages gets
no_engine_rootfrom--target web. See Export for the web, Android and iOS.
Write an entity
An entity is one class that holds synced state, commands, the simulation step and view code. Replace Game.cs with an apple tree: apples grow on their own, and players pick them.
using OneGame.Core; using OneGame.Host; using OneGame.UI; namespace Orchard; /// <summary>An apple tree. Apples grow on their own; players pick them.</summary> [Entity] public partial class Tree : Entity { [Synced] public partial int Apples { get; private set; } [Synced] public partial int Picked { get; private set; } [Synced] public partial int Growth { get; private set; } // A command: the UI and tests send it, the authority checks it and runs it. [Command] public bool Pick(int count) { if (count < 1 || count > Apples) { return false; // refused: the state does not change } Apples -= count; Picked += count; return true; } // Every 30 ticks, zero to two new apples, drawn from a named random stream so a replay draws the same. protected override void OnTick() { if (++Growth < 30) { return; } Growth = 0; Apples += World!.Rng["growth"].NextInt(3); } [View] public string Label() => $"{Apples} on the tree, {Picked} picked"; } public sealed class OrchardGame : GameDefinition { public override string Name => "orchard"; public override void RegisterEntities(EntityRegistry registry) => OneGame.Generated.Orchard.EntityTypes.Register(registry); public override void Setup(World world) => world.Add(new Tree()); public override void DefineScreens(ScreenSet screens) => screens.Add("main", game => Ui.Column( Ui.Label(() => TreeOf(game).Label()).Named("apples"), Ui.Button("Pick one", () => TreeOf(game).Send.Pick(1)).Named("pick").Padding(Length.Px(8)).Background(Color.Rgb(0x2F6B4F))) .Padding(Length.Px(24)).Gap(Length.Px(12)).Background(Color.Rgb(0x1E2A22)).TextColor(Color.White)); private static Tree TreeOf(GameInstance game) => (Tree)game.World.Entities[0]; }
- Synced state is a
[Synced]partial property. The source generator completes it with change tracking, a binary codec, deltas and snapshots. The setters are private, so only the entity’s own sim code writes them. - A command is a
[Command]method. Views and tests call it throughSend(TreeOf(game).Send.Pick(1)), and the authority runs it on the next tick. Returningfalserefuses it. Commands are the only way a view changes the world. - The simulation step is
OnTick, run at a fixed rate. Randomness comes from named streams (World.Rng["growth"]) seeded from the world’s seed, so the draws are part of the state. Floats, clocks,System.Random, threads and I/O in sim code are compile errors. See Deterministic simulation. - A view is a
[View]member. It reads state and never writes it: assigningPickedinsideLabelis a compile error. - The screen is built in
DefineScreenswith a C# builder. Lambdas such as() => TreeOf(game).Label()are bindings, read again every frame.Named("pick")gives an element the id that scenarios and tests use.
Build and run it twice with the default seed, then with seed 7:
onegame build onegame run --headless --frames 300 --json onegame run --headless --frames 300 --seed 7 --json
build succeeded (Debug, 0 warnings, 1.01s) {"ok":true,"game":"orchard","headless":true,"frames":300,"tick":300,"hash":"d0045829889cf126","width":1280,"height":720} {"ok":true,"game":"orchard","headless":true,"frames":300,"tick":300,"hash":"d5ef04c0af6a538b","width":1280,"height":720}
A different seed grows a different tree; the same seed always grows the same one.
Test it with a scenario
Delete scenarios/click-button.yaml, which tests the old counter, and add scenarios/pick-apples.yaml:
name: pick-apples description: Apples grow, picking takes them, and picking more than there are is refused. seed: 7 steps: - expect: ui: { id: apples, text: "0 on the tree, 0 picked" } - click: pick # nothing to pick yet: refused - step: 300 - click: pick - step: 1 - command: { entity: Tree, name: Pick, args: [99] } # more than there are: refused - step: 1 expect: - state: { entity: Tree, property: Picked, equals: 1 } - state: { entity: Tree, property: Apples, equals: 9 } - ui: { id: apples, text: "9 on the tree, 1 picked" }
onegame testPASS pick-apples (36 ms, tick 302, hash 9a447aae324c764b) 1 passed, 0 failed
A scenario clicks elements by id, sends commands, steps frames and checks state and UI. seed: 7 fixes the world’s random streams, so the nine apples left are the same on every run. A failing expectation prints the line, what was expected and what was found, and onegame test exits 1. Automated game testing with scenarios covers the rest of the format.
onegame capture --size 720p --after 300 writes captures/frame.png, the screen after 300 ticks, so you can look at it.
Packages by area
A game references only the SDK. It brings Host, Core, UI and Audio (and Live in Debug builds), and through them the rest of the engine. Every package is version 1.1.2, MIT or Apache-2.0.
| Package | What it is |
|---|---|
RecursiveAI.OneGame.Cli | The onegame command, a .NET tool: create, build, run, test, capture, replay, export. |
RecursiveAI.OneGame.Sdk | The MSBuild SDK for games: references, rules and export targets, so a game builds from nuget.org without an engine checkout. |
RecursiveAI.OneGame.Core | The entity runtime: fixed-point numbers, random streams and the attributes that declare entities. It carries the source generators and analyzers. |
RecursiveAI.OneGame.Sync | Nodes, scopes, saves, the command log, replay and drift hashes. |
RecursiveAI.OneGame.Net | Reliable UDP and WebSocket transports, and a network conditioner for tests. |
RecursiveAI.OneGame.Server, .Server.Live | The headless server host (fixed-step scopes, YAML configuration, the plugin API) and its live control. |
RecursiveAI.OneGame.Host | The game host: window, input, fixed-step simulation, UI and render stage, windowed or headless. |
RecursiveAI.OneGame.Platform | Windows, input, controllers and the app lifecycle per operating system. |
RecursiveAI.OneGame.UI, .UI.Kit | The retained UI tree with CSS-model styling and flex layout, and the component kit: buttons, tables, lists, tabs, dialogs, text input. |
RecursiveAI.OneGame.Text, .Atlas | Fonts, shaping and the glyph atlas; the SVG and image atlas for the 2D renderer. |
RecursiveAI.OneGame.Render2D | Sprites, tilemaps and text. |
RecursiveAI.OneGame.Render3D, .Gltf | The physically based 3D renderer, and glTF 2.0 import that is safe on untrusted files. |
RecursiveAI.OneGame.Gpu, .Gpu.Vulkan, .Gpu.Metal, .Gpu.WebGpu, .Gpu.Backends | The graphics layer and render graph, its three backends, and the package that picks one for the machine. |
RecursiveAI.OneGame.Audio | Audio device output and the audio service. |
RecursiveAI.OneGame.Steam | Steamworks bindings, Steam Input, and store-neutral achievements, stats, cloud saves and presence. |
RecursiveAI.OneGame.Purchases | In-app purchases over StoreKit, Google Play Billing and a scriptable fake store. |
RecursiveAI.OneGame.Control, .Live | The control layer the command line, tests and live control share; the development tools of a Debug build. |
RecursiveAI.OneGame.Testing | The C# test API: in-process clusters, scenarios, recordings, captures and determinism checks. |
What onegame doesn’t do
- No editor. That is the design: everything is code and text, driven from the terminal, tests and live control.
- No physics engine. Collision and movement are your own sim code. The 3D scene docs list physics under “not built.”
- Not an ECS. Entities are classes that hold their own state and logic. For hot loops over many rows there are
[HotData]struct-of-arrays tables. - Web, Android and iOS exports need an engine checkout at 1.1.2, as do Play Billing in Android builds and plugin servers. Desktop exports and dedicated servers work from the packages.
Where next
Cookbook recipes, each run on Linux against the 1.1.2 packages:
- Automated game testing with scenarios, record and replay a game, screenshot tests
- Test a game from C#, start tests from fixtures and snapshots, drive a running game over MCP
- Deterministic simulation, client/server sync with a dedicated server, client-side prediction
- Build a game screen with the UI kit, controller and keyboard navigation, check UI contrast
- Export for Linux, Windows and macOS, ship for Steam and the Steam Deck, export for the web, Android and iOS
The guides:
- Your first game: the scaffold line by line
- Driving a game: scenarios, captures, replays, live control, servers and clusters
- The UI guide: screens, styles, the component kit, focus and theming
- Exporting and checking on devices
- Changelog
Coming from another engine: onegame for MonoGame developers and onegame for Unity developers.