Tools That Share the Runtime

Content tooling is the part of engine work that never appears in a portfolio and decides how fast a team can actually make things. A particle effect that takes ten minutes to see in context gets iterated on twice. One that updates while it's being edited gets iterated on fifty times, and the fiftieth version is better.

Across my engines the tools went through two shapes. Five separate editor executables, then a tool framework living inside the game. Both were improvements on writing content by hand, and the second one is the arrangement I'd build again.

This is the fourth post taking one system at a time out of those engines.

Shape One: Editors in the Same Solution

The second engine had five editors, each a separate executable: animation, GUI, material, particles, and a dungeon designer. Plus an asset packager.

The decision that made them work wasn't that they existed. It was that they were projects in the same solution as the engine, built by the same build, linking the same libraries.

A tool built that way consumes engine types directly. The particle definition the editor manipulates is the struct the runtime simulates, not a copy of it, and not a serialized description of it. When the engine's definition changes, the editor fails to compile.

That compile failure is the feature. The alternative arrangement, where a tool is a separate program reading and writing a file format, has a gap in it: the tool's understanding of the format and the engine's understanding of the format are two pieces of code that agree by convention. They drift, and the drift shows up as content that loads wrong rather than as a build error. Sharing the type deletes the gap.

The cost is real. The solution takes longer to build, and a broken tool breaks the build for the game. My opinion is that's a good trade at this scale, and I'd reconsider it on a team of twenty where a tools programmer's mistake shouldn't stop gameplay work.

Shape Two: Tools Inside the Game

The current engine moved the tools into the game, as classes over a common base:

virtual void Init() = 0;
virtual void Draw() = 0;
virtual void SaveSettings(Json::Value& settings);
virtual void LoadSettings(Json::Value& settings);

A tile editor, a tile recipe editor, an entity editor, and a gradient editor all derive from that. They run in the game's process, in the game's renderer, against the game's live data.

Most of this gets better, and one thing gets worse.

Authoring happens in the renderer that ships. A separate editor draws its own approximation of what the game will look like, and the approximation is always slightly wrong. Lighting, shaders, and post-processing are the parts most likely to differ and the parts most likely to matter to whoever is placing content. In-game editing removes the question of whether the preview is accurate, because the preview is the game.

Edits are visible immediately, in context, next to everything else. Not a rebuild, not a reload, not a repack.

And a tool becomes cheap to add. A new editor is a class with two required methods, and it inherits the window management, the input routing, and the settings persistence. Five editors as five executables meant five copies of the shell around each one.

What gets worse is that tool code ships in the game binary unless it's compiled out, and tool bugs are game bugs. That's a genuine cost, and it's the reason for the compile-time switch that removes the whole framework from a release build.

Persisted Tool State

SaveSettings and LoadSettings on the base class are the detail I'd point at for anyone building a tool framework.

Every tool remembers where its panels were, what was selected, which mode it was in, and what the last used values were. Not because any single tool needed it enough to build it, but because the base class provides it and the derived tools implement two short methods.

Tool state is one of those things that never makes a task list and dominates how a tool feels. An editor that forgets everything on restart imposes a small setup tax on every session, and the tax is paid by the person using it most. Putting the mechanism in the base class means the cheap thing to do is also the right thing to do, which is the only reliable way to get a detail like this done consistently.

The choice of format matters less than the placement. It's JSON here because the engine already had a JSON dependency. The point is that it's the framework's responsibility rather than each tool's.

Loose Files Against the Pack

Underneath both shapes is one asset-loading decision that made iteration possible at all.

The asset manager reads from a compressed pack, and a pack can be flagged to allow unpacked assets. When it is, a lookup that misses the pack index falls back to looking for the file loose on disk beside the pack, and if it finds one it opens it and adds it to the index.

That means a development build with no pack at all serves everything from loose files. Edit a texture, restart, see the change. No repacking step between making a thing and seeing it.

I'd change the precedence if I built it again. The pack is checked first and loose files are the fallback, so once a pack exists it wins. The more useful arrangement is the opposite: loose files override the pack, so a developer can drop a single modified asset next to a full pack and have it take priority. That turns "iterate on one asset" from a full unpacked build into one file.

What the Tools Actually Changed

What to measure is not the tools, it's what having them did to the content.

The engine with five editors carried four completed game projects. The engine before it, with no tools, carried one. That's not entirely attributable to tooling, but the direction is the direction I'd expect: content that's cheap to author gets authored, and content that requires a programmer gets specified, argued about, and cut.

A tool converts a request into a task somebody can do themselves. That's the whole return, and it's why tooling deserves engine-quality attention rather than the leftover hours.

What Transfers

Build tools against the same types the runtime uses, in the same build, so a divergence is a compile error rather than a content bug.

Put authoring in the shipping renderer once that's possible, because a preview that approximates the game will be wrong in exactly the ways that matter to the person authoring.

Make tool state the framework's job. It's the difference between a tool that's tolerated and one that's used.

And make loose files beat the packed ones, so iterating on a single asset costs one file rather than a full rebuild of the pack.