← Blog

2026 September Release

2026 September Release

2026-10-01

A year or two ago I decided to make a game using only technology I had built myself. That meant I needed a game engine. I wanted to work the way I was used to, in a Unity-like editor with designer tools around the scene, so the engine needed a studio. The studio needed a GUI, which became Guinevere. Along the way I realised I would rather build on a platform like VSCode, which already provides much of that groundwork, than write one more single-purpose editor. So the studio became a generic host for plugins, called Gaya, and Turian became one of its plugins. Together with the game, that stack is MEGA4.

A few months ago I got stuck and went exploring in Zig. I've made great progress and that engine lives on as TurianZ, and Coming Home to C# tells why I came back. The return took the last two months.

In the 1.0 post I joked that I would not be surprised to see v23 in a couple of months. A half dozen breaking changes to reach v2. Two of the gaps I listed in that post, no undo in Studio and prefab copies not linked to their source, are now closed.

Turian Studio 2.0 with the main menu expanded across the application bar
The same Studio frame with the main menu collapsed into a single button

Why 2.0, and Why Today

Turian follows semantic versioning: the first number, the major version, only goes up when a change can break your project. It is a promise in a number. If you see 2.3 after 2.0, you can upgrade without reading anything. If you see 3.0, read the guide first.

Six of September's changes break that promise for 1.x projects:

Change What breaks Upgrade steps
Camera rigs and in-game UI became bricks projects that use them must declare them Bricks
Dependency injection replaced global services static Input, Localization, RuntimeServices Services
Transform became an immutable value node.Transform.Position = … Transform
Inspector attributes moved to a shared package full attribute names, [HideInEditor] Attributes
IdClass renamed to IdObject plugins using the identity API IdObject
Application bar and appearance moved into Gaya custom Gaya hosts and chrome Gaya

I tagged 2.0.0 on September 30th, as soon as the first of them landed, and pulled the tag the same day. More breaking work was already in flight. Shipping it as 2.0, 3.0 and 4.0 in one week would have given you three migrations for one month's work. So the breaks shipped together today, with one migration guide. Scene, prefab and data asset files keep their ids, so none of your content needs converting. From here on, 2.x only gets compatible changes. The next break, whenever it comes, will be 3.0.

Bricks: An Open Package Format

Turian 2.0.0, tickets #79, #74

The biggest piece of 2.0 is bricks. A brick is a package: code, assets and editor tools in one folder, with a version, dependencies and a license, that any project can install, update or remove. Unity has its Package Manager and Godot has its Asset Library. Bricks are our answer, with one difference: they are not tied to Turian.

Bricks belong to Gaya, the platform under Studio, and their format is an open spec: a package.json manifest, a lock file, and a registry index, all documented with JSON schemas. Any engine, or any application that is not an engine at all, can read and publish them. Turian is simply the first user. It is also how we will ship things from now on: example projects, game mods and Studio extensions will all be bricks, so you can stack each other and build upon.

The Bricks panel listing the bricks on this machine, with the Inspector showing one brick's manifest

What a brick can carry

A brick can hold anything a project folder holds:

  • Scripts, compiled under their own assembly definition, so a brick's code stays isolated from yours.
  • Assets: models, textures, prefabs, scenes, sounds, DataAssets. Each ships with a .meta file that carries its permanent id, so a scene that uses a brick's prefab keeps pointing at it across updates.
  • Studio extensions: menus, panels and Inspector pages. The producer example's com.acme.inventory brick adds an Acme menu to Studio the moment it is installed.
  • Prebuilt code. turian-cli brick pack --precast compiles the brick's scripts ahead of time, so the people who install it load ready-made assemblies instead of compiling your code.

The engine eats its own bricks

The first bricks are Turian's own. The Follow, Orbit, Free Fly and FPS camera rigs moved out of the engine core into org.mass4.turian.cameras, and the in-game UI (.ui documents, .uss style sheets and everything that draws them) moved into org.mass4.turian.ui. Both ship with Turian and new projects install them, but a game that does not need them can remove them. A game without UI no longer ships Guinevere or Skia, and its executable loses libSkiaSharp, about 13 MB per platform.

When a brick is missing, nothing is destroyed. Its components load as placeholders, its assets are skipped and their .meta files are left alone, so installing the brick again brings everything back. 1.x projects need to declare these two bricks: see the migration guide.

Installing and managing bricks

A project lists its bricks in Bricks/manifest.json. Next to it, packages-lock.json records exactly what was resolved, down to the commit or the file hash, so every machine on the team builds the same game. Commit both. A brick can come from five places:

Source Example Use it for
Built in builtin:org.mass4.turian.cameras bricks that ship with the engine
Folder file:../bricks/com.acme.levels a brick you are developing next to your game
Packed file file:levels-1.0.0.brick a brick someone sent you
Git git+https://github.com/acme/rules.git#v1.2.0 a brick in a repository, pinned to a commit
Registry ^1.0.0 the newest compatible release from a registry

Bricks are stored once per machine, in ~/.gaya/bricks, and never copied into projects, so ten projects using the same brick share one copy on disk. Set GAYA_BRICKS to keep the store elsewhere, such as in a CI cache.

In Studio, Project → Bricks… opens the Bricks panel. It lists what the project installs and why each brick is there, meaning what depends on it, and installs, updates, removes or embeds bricks without leaving the editor. The Inspector shows the selected brick's manifest. The Assets panel gains a Bricks folder next to Assets, so you can drag a brick's prefabs, textures and scripts into scenes and Inspector fields like any other asset. Everything also works from a terminal: turian-cli brick add, remove, list, update, search, and restore --locked, which fails a CI build when the manifest and the lock disagree.

Changing what you installed

Installed bricks are read-only, because an update would otherwise wipe your edits. Instead, there is a ladder of ways to make a brick your own, from lightest to heaviest:

Step How Follows the brick's updates?
Use it as it is reference it by id yes
Make a variant a prefab variant or a data asset variant that overrides a few values yes, except what you overrode
Change how it imports ProjectSettings/PackageImportOverrides.json yes
Copy one asset Copy in the Assets panel, or brick copy no: the copy gets a new id
Fork the whole brick brick embed you merge them with brick rebase

An embedded brick is a writable copy in Bricks/<id>/ that wins over the installed one. brick diff shows what you changed, and brick rebase merges the original's next release into your fork, file by file, marking conflicts where you both edited the same lines, the way git does.

Making your own

turian-cli brick new user.you.inventory creates a brick with a manifest, an assembly definition and a first script. Ids are written in reverse-DNS order, like Java and Android packages: use a domain you own (com.acme.inventory), or user.<name>.… if you do not have one. brick verify checks that every asset ships a unique .meta, and brick pack writes a .brick file with its hash. Packing is reproducible: the same sources always produce the same bytes, so anyone can check that a published brick matches its source code. A reusable GitHub workflow, MASS4ORG/Turian/.github/workflows/brick.yml, verifies, packs and releases a brick for you.

Three example projects show the whole cycle. example-06-plugin-producer authors three bricks, one of them with editor code; example-05-plugins installs them; and example-07-team-split covers teams. When one team builds a brick and another builds a game on it, brick stub makes a placeholder with the same asset and type ids, so the game references the real ids from day one, and brick verify --against fails when the stub and the real brick drift apart.

Registries

A registry is where people publish bricks for others to find. A Turian registry is a static website with a signed index, so GitHub Pages is enough to host one, with no server to run. Add one to a project with turian-cli brick registry add studio https://… --scope com.studio --key studio.pub, and a plain version range such as "com.studio.inventory": "^1.0.0" resolves to the newest compatible release.

Every release is signed. Publishers sign with ed25519 keys (turian-cli brick publish --registry site/ --key registry-key), a modern elliptic-curve signature scheme and the same kind of key GitHub accepts for SSH logins, so most developers already have one. A project only trusts the keys it lists itself, and the lock file records the version, the file's hash and the key that signed it. If a registry is ever compromised, a tampered brick fails to install instead of silently landing in your game. user.<name>.… names belong to whoever publishes under them first.

Bring a Unity package over (very experimental)

turian-cli import unitypackage Crates.unitypackage --brick user.you.crates --out bricks/ reads a .unitypackage directly, with no Unity installed, and turns it into a brick. Textures, models and audio keep their Unity ids, and simple materials and prefabs come along. Treat it as an experiment, not a migration path: scripts, shaders and animation are not converted, and anything beyond simple scenes will need hand work. The report lists what was skipped. Check the license of what you import, too: the tool moves files, it does not change their terms.

No More Globals: Dependency Injection

Turian 2.0.0, ticket #44

Unity made a choice in its first years that still haunts it: its API is global. Input.GetKey, Time.deltaTime, Camera.main, GameObject.Find are static calls anyone can make from anywhere, and most Unity projects add their own GameManager.Instance singletons on top. It is wonderfully convenient on day one. Years later, it is why a Unity component is hard to test on its own, why turning off domain reload to make Play Mode faster means hunting down every static field that remembers the previous run, and why the community built Zenject and VContainer to work around it. Unity has never been able to remove its statics, because every project in the world depends on them.

Turian 1.0 had the same convenience, behind static classes such as Input and Localization. 2.0 removes it while there are still few projects to break. Dependency injection means a component does not go looking for the things it needs; it declares them, and the engine hands them over. Each session (the exported game, every Play Mode run, every test) gets its own container of services, built on .NET's standard Microsoft.Extensions.DependencyInjection:

public class Patrol : Component
{
    [InjectService, JsonIgnore]
    public IInputSource? Input { get; private set; }

    public override void OnUpdate(float deltaTime)
    {
        if (Input?.IsKeyDown(Key.W) == true)
            Node.Position += Vector3.UnitZ * deltaTime;
    }
}
Unity Turian 2.0
Read input Input.GetKey(KeyCode.W) from anywhere inject an IInputSource
Your own services singletons, or Zenject or VContainer IEngineServiceModule, built in
Play Mode isolation static fields survive unless the domain reloads every session has its own services
Test a component alone replace globals by hand give it a test container
Designer-owned services ScriptableObject singletons by convention a DataAsset, shared per session

[InjectService(Optional = true)] lets a component run where a service is absent, such as a scene previewed in Studio with no input routed. Games and bricks register their own services by implementing IEngineServiceModule, and Turian discovers the modules in every assembly the game loads. SceneTicker.TimeScale arrives alongside: it scales game time, 0 freezes it, and unscaled time keeps running for menus and effects. Upgrading from 1.x takes a few replacements, listed in the migration guide.

The table's last row is the designer's side of the same story, and it deserves its own section.

DataAssets: The Designer's Backbone

Turian 1.0 to 2.0, tickets #145, #146, #147, #43, #197

A DataAsset is Turian's take on Unity's ScriptableObject: a C# class whose instances live as files in your project, edited in the Inspector instead of in code. A WeaponData asset holds a sword's damage and speed; a LevelData holds a level's enemy waves. Programmers write the class once, and designers create, tune and balance as many assets as they like without touching a script.

In 2017, Ryan Hipple showed at Unite Austin how far this idea goes, in a talk called Game Architecture with Scriptable Objects. Instead of hard-wiring systems together in code, you connect them through assets: the player's health is a FloatVariable asset that the health bar, the audio and the save system all read, and "player died" is a GameEvent asset that anything can listen to. Designers rewire the game by dragging assets in the Inspector. The approach became known as SOAP, the ScriptableObject Architecture Pattern, and it is one of the most designer-friendly ways to build a game. Turian is doubling down on it: DataAssets will be the backbone of how designers work, and September laid the foundations.

Unity ScriptableObject Turian DataAsset
One instance per asset yes yes, shared per session
Changes in Play Mode persist after Play Mode ends thrown away; the authored file is never touched
Variants no, without third-party tools yes, like prefab variants
Project settings special files in ProjectSettings/ DataAssets themselves
Reference scene objects silently lost when saved refused at compile time, with a suggestion
  • One shared instance. Every field, scene and service that references a DataAsset gets the same object, so a GameManager DataAsset works as a service: change its coins in one scene and the next scene sees them. A second session, such as another Play Mode run or a test, gets its own copy.
  • Authored data stays authored. Play Mode starts from the saved values every time, and nothing a running game does is written back to the file. In Unity, changes a running game makes to a ScriptableObject outlive Play Mode, a classic way to lose an afternoon of balancing. DataAsset.Instantiate gives a script a private copy when it needs one.
  • Variants. A DataAsset can be a variant of another one, storing only the values it overrides, the way a prefab variant works. A "Fire Sword" can be a variant of "Sword" with more damage, and when you rebalance "Sword", every variant follows. Create one in Studio or with turian-cli variant.
  • Settings are DataAssets. The project's Input, Graphics, Player and Localization settings are DataAssets, created from New → Settings and edited in the Inspector like everything else. That means settings can be variants, live in bricks and be read through the same services as your own data.
  • Hardening. Serializers are generated at compile time instead of discovered by reflection at runtime, so DataAssets load faster, and they can be preloaded in the background. Variants refuse anything that would quietly change what they are: an override cannot replace the asset's id or type, a variant must keep its base's type, and unknown syntax fails loudly instead of being ignored. A DataAsset that tries to hold a reference to a scene object is reported by the compiler (TUR0001), with a suggestion for what to use instead.

Prefabs That Stay Linked

Turian 1.1.0, tickets #84, #185

A prefab is a reusable piece of a scene, such as a lamp post, an enemy or a whole house, that you build once and place many times. In 1.0 a placed prefab was a copy: change the original, and the copies did not notice. Now each instance is a link, and the workflow is the one Unity users know:

  • Overrides are tracked. An instance remembers what it changes about its prefab: values, added components and nodes, and removed ones. The Scene Tree and the Inspector mark them.
  • Apply and revert work per value, per component or for the whole instance. An apply writes to the prefab that owns the value, innermost first, so an edit to a nested prefab lands where it belongs.
  • Nested prefabs and variants. A prefab can contain instances of other prefabs: a house prefab full of lamp prefabs. A prefab whose root is an instance of another prefab is a variant of it, such as a "Red House" that inherits everything from "House" except its color.
  • Prefab mode. Open an instance's prefab from the scene, edit it, and follow the breadcrumb back. Instances in the open scene refresh as soon as the prefab changes.
  • Unpack breaks the link and keeps the hierarchy as plain nodes, optionally unpacking nested instances too.

Every one of these operations is undoable. Scene files stay small, too: an instance is saved as a link to its prefab plus what it overrides, adds and removes, instead of a full copy of the prefab in every scene that uses it.

Undo, Redo, and Nothing Lost

Turian 1.1.0, ticket #81

Studio now has undo and redo for every editor operation, and it works the way Unity's does: one history for the whole project, where every step belongs to a document. A document is an open scene or an asset the Inspector is editing. Undoing a step brings its document to the front first, so you always see what changed.

Undo tracks what you meant, not every tiny event. Dragging an object with the gizmo is one step however many frames it lasts, and scrubbing a number field merges into one step. Saving seals the step, so the edit after a save always undoes separately. Changes beyond the scene count too: renaming or moving files in the Assets panel, or rewriting a prefab with Apply. The Edit menu names the step it is about to undo or redo. While Play Mode shows the running game, history is locked, because those changes are thrown away anyway.

Quitting Studio or switching projects with unsaved work now asks first.

Performance

Turian 2.0.0, tickets #32, #132, #58

One word comes up in every table below: allocation. In C#, every object created on the heap is memory the garbage collector must later find and free. A game that allocates every frame eventually pauses to collect, and the player sees a hitch. Zero allocations per frame means no hitches from the engine.

A transform that stopped allocating

It started as a question: should Turian split its Node into specialized 2D, 3D and UI types, as some engines do? That would ripple through components, serialization, prefabs and tools, so I measured before deciding.

The answer was elsewhere. Every node's Transform (position, rotation and scale) was a class with change notifications: every edit raised an event, and every read of a world position walked the hierarchy and allocated. In 2.0, Transform is an immutable struct, a small value stored inside the node rather than a separate object, and each node caches its world transform until it or a parent moves.

On a 100,000-node tree, moving every node and then reading every world position:

Frame time Allocated per frame Memory per node
1.x: class Transform 75.9 ms 10 MB 556 bytes
2.0: struct Transform 6.5 ms 0 342 bytes
Flat 3D storage prototype 0.60 ms 0 85 bytes
Flat 2D storage prototype 0.36 ms 0 53 bytes

That is 11.7× faster with no garbage at all. The last two rows are prototypes that store every node's data in flat arrays. They show where the 2D/3D split would actually pay off, so they are kept as the target for a future major, and Node stays one type for now. Upgrading code that writes transforms is mostly search and replace; see the migration guide.

A render list that stays put

The renderer used to rebuild its list of things to draw by walking the whole scene every frame. It now keeps the list, updates only what changed, and sorts it by material so the GPU switches state less often. On the Bistro sample, the Amazon Lumberyard street scene, over 300 frames:

Bistro, 300 frames Median render Allocated per frame
Before 7.2 ms 797 KiB
After 2.1 ms 0.1 KiB

Measuring what not to adopt

ZLinq, a popular allocation-free replacement for LINQ (C#'s query syntax for collections), was a candidate for scene queries. Benchmarks on 50,000 nodes said no:

Gameplay query, 50,000 nodes Standard LINQ ZLinq Plain loops
Count enemies near the player 1.8 ms 3.3 ms 1.7 ms
Find a node by name 1.5 ms 1.9 ms 0.35 ms
Nearest pickup 1.9 ms 4.5 ms 1.7 ms
10 weakest enemies 2.0 ms, 40 KiB 4.6 ms 1.6 ms, 0 KiB

Once the scene walk itself stopped allocating, plain loops won every query, with no new dependency. Turian instead gained overloads such as GetComponentsInChildren<T>(List<T>) that fill a list you pass in, so your gameplay code can query every frame without allocating. Guinevere went through the same exercise for its layout engine, with its own numbers below.

Forms That Write Themselves: Autoformers

Turian 1.2.0 and 2.0.0, tickets #28, #29, #194

Write a C# class, and Studio's Inspector shows its fields as a form: a slider for a [Range], a checkbox for a bool, a color picker for a color. Turian 1.2 made that machinery extensible with property drawers, which change how a type or an attribute is drawn, along with [Tooltip] and [InspectorOrder].

Then I noticed that nothing in that machinery was about games. Any tool built on Gaya wants forms generated from C# classes. So in 2.0 the form generator moved into Guinevere as Autoformers, and the attributes moved into a shared MASS4.Attributes package. Other GUI applications can now use them, and Gaya already does: its Settings panel is generated automatically from C# settings classes, including custom presentation and validation. Studio's Inspector and the Bricks Inspector are Autoformer forms too. Turian keeps only the attributes that are about games. Attribute names changed a little on the way; see the migration guide.

A Studio With Less Chrome

Studio's main menu now collapses into a single button in an application bar, which it shares with the project switcher and the play controls. That bar can also replace the operating system's title bar: with custom decorations, empty space in the bar drags the window, its own buttons minimize, maximize and close it, and the window resizes from every border and corner.

A new Appearance page in Gaya holds theme, text size, zoom and window decorations together, and the theme dropdown and View → Themes share the saved choice. Studio ships with five themes: Dark, Light, Dark Contrast, Darcula and Catppuccin.

Studio's Appearance settings and View, Themes menu in the Catppuccin theme
The same Studio frame in the Light theme

Custom Gaya hosts and plugins that contribute window chrome have two small changes to make; see the migration guide.

Engine Guts

Turian 2.0.0, tickets #63, #197

Bricks make content composable, and that raised a question: what keeps a game stable when its content comes from many places? 2.0 writes down the answers, in code and in tests.

  • Content overlays. Content packages mount in dependency order. Conflicts, cycles and missing requirements are reported before the game starts, and a running game keeps the content it started with. This is the ground mods will stand on.
  • Runtime saves. The new RuntimeSave writes a versioned snapshot of a running game: its tick, its random state and its game state. Before restoring, it checks the snapshot against the game's content, so a save made with a different set of mods is caught instead of loaded wrong. Upgrades of old saves run on a copy, so a failed one leaves the save untouched.

Guinevere: Stronger Foundations

Studio is drawn by the Guinevere GUI library, so most of what you see in Studio is Guinevere work, and Guinevere had a hell of a month: from 1.6 on September 9th to 5.6 on October 2nd. Its packages are now published as MASS4.Guinevere.*. The highlights:

  • Excalibur controls. A dedicated control library on top of the core, published as its own package: virtualised tree views that draw only the visible rows, sliders, Unity-style scrubbable number fields, tabs, toasts, tooltips, menu bars, context menus, cascaded flyouts, modal dialogs, and a file browser. Colors and sizes are themeable value by value.

  • Docking. gui.DockSpace with tab groups, nested splits and floating windows, drag-to-dock, tab reordering and tear-off, plus splitters between any two panels. Studio's whole layout is built on it.

  • Autoformers. Turian's form generator found a new home in Guinevere; see above.

  • A faster layout engine. Sizes are now composable expressions: pixels, percentages, ratios, expansion and fit-to-content can be added, scaled and animated between, so a panel can smoothly grow from "200 pixels" to "70% of the window". At the same time, layout stopped allocating and scales linearly on deep trees:

    1,000-node scenario Before After Allocated per layout
    Wide wrapping 0.035 ms 0.011 ms 45 KB → 0
    Nested fit-content 0.132 ms 0.036 ms 55 KB → 0
    Measured text runs 0.035 ms 0.011 ms 52 KB → 0
  • Events and cursors. Mouse and keyboard events travel through the interface the way they do in a web browser, down to the target and back up, and any handler can stop them. Widgets declare their own cursor shapes, and the pointer can be hidden or locked, which a 3D viewport needs for mouse-look.

  • Platform capabilities. Integrations (OpenGL, Vulkan, OpenTK and Raylib) declare what they can do, such as cursor shapes, pointer locking, window movement or vetoing a close. Gaya and Turian use Vulkan, the most modern of the four.

Guinevere's application bar changed shape on the way; code that uses it directly should follow the migration guide.

Studio's Appearance settings and View, Themes menu in the Catppuccin theme
The same Studio frame in the Light theme

Looking Ahead

There are a lot more fixes, tweaks and enhancements not listed here. For the complete list, see the changelog.

The releases page has Turian 2.0.0 for Windows and Linux, the migration guide walks a 1.x project through the upgrade, and the documentation has a new page on bricks. If something breaks, tell me on GitHub. If you want to help fund the next release, you can find me on Patreon or Ko-fi.

#release#engine#studio#dotnet