Build Log

Building an Asset Browser for Reactive SO (because the Project window wasn't enough)

  • Unity
  • Reactive SO
  • Editor Tooling
  • UI Toolkit
  • Game Development

Reactive SO’s whole pitch is that a Player component doesn’t need a reference to HealthBar, GameManager, or anything else that cares about damage — it just writes to an IntVariableSO and raises a VoidEventChannelSO, and whatever’s listening finds out on its own. That pitch holds up.

What it doesn’t advertise up front is where the coupling actually goes. It doesn’t disappear. It moves into the Assets folder, as .asset files with no compiler checking what any of them mean.

I didn’t feel that cost until a project had enough of these assets that a Project window search stopped being enough. t:IntVariableSO finds every int variable in the project. It doesn’t tell you which of the three that are all some variant of PlayerHealth is the one four other scripts already reference, or whether adding a fourth is safe.

This post is about that cost, and about the Asset Browser window I built to live with it, part of Reactive SO since v2.1.0. It isn’t a story about a tool fixing a problem, because it doesn’t. Reactive SO’s own documentation says as much directly, and that’s where I want to start.

Unity Asset StoreReactive SO | Game ToolkitsScriptableObject-based reactive architecture for Unity. Variables, Event Channels, Runtime Sets, GPU Sync, Reactive Entity Sets, and dedicated debugging windows.

A cost the docs already admit to

Reactive SO’s user guide has a page on Asset-based Dependency Injection, and under its list of trade-offs for using ScriptableObjects this way sits a paragraph I wrote for the documentation, not for this blog:

Asset Management Hell - This is real. A mature project using this pattern will have dozens or hundreds of assets: OnPlayerDeath, OnEnemySpawned, PlayerHealth, EnemySet… Finding the right asset, avoiding duplicates, and maintaining naming conventions becomes a genuine challenge. Reactive SO provides tools like Asset Browser and Dependency Analyzer to help, but the overhead is unavoidable.

That subsection closes with the line that names what all three trade-offs add up to:

Reactive SO chooses ScriptableObjects because they excel at cross-scene shared state and decoupled communication - the specific problems this architecture addresses. But be aware: you are trading code complexity for asset complexity.

What is Reactive SO already covers one shape of that trade, through the Dependency Analyzer: once everything talks through assets instead of direct references, you can lose track of what depends on what. This post is the other shape. Not losing track of relationships between assets — losing track of the assets themselves.

OnPlayerDeath, OnEnemySpawned, PlayerHealth, EnemySet are the guide’s own examples, and they’re exactly the kind of name a project ends up with three variants of: PlayerHealth, Health_Player, PlayerHP, all three still sitting in the project because a Project window search only ever confirmed a type, never a role.

PostWhat is Reactive SO?An introduction to Reactive SO, a ScriptableObject-based reactive architecture for Unity. Built on Ryan Hipple's Unite Austin 2017 patterns, extended with Reactive Entity Sets, GPU Sync, and dedicated debugging windows.

What the Asset Browser actually does

Window > Reactive SO > Asset Browser was added in v2.1.0. The commit that landed it, and the commit that merged three separate Monitor windows into one, went in about an hour apart the same afternoon in early January.

The user guide calls the browser a successor to the Variable Monitor’s state overview functionality from v1.1.0. That’s the cleanest way to keep the Asset Browser and the current Monitor window apart: the Monitor is a chronological event log for Play Mode, the Asset Browser is a live inventory of what exists whether or not you’re playing.

PostRebuilding the Reactive SO Monitor from three windows into oneI originally said Reactive SO has three dedicated Monitor windows. It doesn't anymore. Here's why I collapsed them into a single tabbed window, and the CSV export rabbit hole I fell into on the way.

The window is one generic core, AssetBrowserCore<TAsset>, wrapped by five tabs — Event Channels, Actions, Variables, Runtime Sets, Reactive Entity Sets. Every tab shows Name and Type columns, with the type name trimmed for reading (VoidEventChannelSO becomes Void, IntVariableSO becomes Int), plus a text search and a type filter dropdown scoped to whichever tab is open.

Three of the five add one more column for whatever changes on its own during Play Mode: Variables show a live Value, Runtime Sets and Reactive Entity Sets show a live Count. Event Channels and Actions get a button instead — Raise or Execute — enabled only while Application.isPlaying is true.

Double-clicking a row (or pressing Enter) still pings the asset in the Project window, same as it always did; the browser doesn’t replace that step, it just gives me a reason to reach for it less often.

Raise and Execute only go so far. A VoidEventChannelSO takes no argument, so Raise fires it directly. Anything typed — an IntEventChannelSO, an ActionSO<T> — needs a value the row has no field for, so clicking Raise or Execute on one of those selects and pings the asset instead, and the actual firing still happens from the Inspector’s manual-trigger section.

The browser gets me to the right asset faster than scrolling the Project window would. It can’t skip the Inspector once a parameter is involved.

Dragging into the Inspector

Every tab also lets me drag a row straight into an Inspector field, instead of pinging the asset and then dragging it from the Project window as a second step. All five tabs wire this up the same way, through MultiColumnListView’s drag events:

// EventChannelTab.cs (excerpt — identical in Action, Variable, RuntimeSet, and ReactiveEntitySet tabs)
listView.canStartDrag += OnCanStartDrag;
listView.setupDragAndDrop += OnSetupDragAndDrop;

private bool OnCanStartDrag(CanStartDragArgs args)
{
    return args.selectedIds.Any();
}

private StartDragArgs OnSetupDragAndDrop(SetupDragAndDropArgs args)
{
    var startDragArgs = new StartDragArgs(args.startDragArgs.title, DragVisualMode.Copy);

    var assets = args.selectedIds
        .Where(id => id >= 0 && id < core.FilteredAssets.Count)
        .Select(id => core.FilteredAssets[id].Asset)
        .Where(a => a != null)
        .Cast<Object>()
        .ToList();

    startDragArgs.SetUnityObjectReferences(assets);
    return startDragArgs;
}

That block is copy-pasted across all five tab classes rather than shared through the generic core — the kind of duplication I’d clean up if I touched this file again. What’s worth saying about it instead: it was there from the first line of every tab.

The note I’d left myself about needing a list window for every asset type is dated January 1st; the commit that closed it, the next day, landed the shared core, the Event Channels, Variables, Runtime Sets, and Reactive Entity Sets tabs, and their tests in one pass, drag-and-drop already wired into all four.

The fifth tab, Actions, arrived four days later, bundled with the ActionSO feature it browses — and it shipped with the same drag-and-drop wiring in its own first version, not added on afterward. There’s no earlier version of any tab that shipped without it, and nothing in the history shows me trying something else first.

By the time I sat down to build each one, I already knew what it needed to do — which says more about how well understood the underlying problem already was than about how the window got built.

What it doesn’t fix

None of that changes the count. The Asset Browser doesn’t stop a project from accumulating three ScriptableObjects that all mean “the player’s health,” and it doesn’t enforce a naming convention on any of them — it makes the three easier to find once they already exist, which is a smaller claim than it sounds like.

The user guide’s own wording is that the overhead is unavoidable, not that it’s solved, and I don’t think a debugging window changes which of those is true.

Structurally, the window can only ever be a faster way to reach the Inspector, not a replacement for it. There’s no bulk rename, no merge-these-duplicates action, no enforced convention on what a new asset gets called. Those stay manual, same as they were before the browser existed.

Things still bugging me

  • The 200ms refresh. Variables, Runtime Sets, and Reactive Entity Sets poll on an EditorApplication.update hook (REFRESH_INTERVAL = 0.2) while in Play Mode. Event Channels and Actions don’t, because their only Play Mode affordance is a button, not a value that changes on its own. The number itself isn’t backed by anything — it felt fine and I never went back to check whether 100ms or 500ms would’ve felt just as fine.
  • The type filter doesn’t scale. It’s a flat, alphabetized dropdown with no grouping. Event Channels alone ship 12 built-in types before a project adds a single custom one; add even a few custom channel types, which typed-event projects always do, and the same flat list clears 20 without trying.
  • The list is a drag source, not a drop target. There’s no way to drag an asset onto a row to reassign something. Every drag still ends in an Inspector field — one direction only.
  • SetUnityObjectReferences is already marked obsolete in current Unity documentation, in favor of SetEntityIds. It still works on every version this package supports. I haven’t touched it.

Wrap-up

Asset Management Hell isn’t something this project ran into by surprise. It’s named, in the past tense, on the same page that explains why Reactive SO reaches for ScriptableObjects in the first place.

That’s most of why the Asset Browser took a day to build: there was nothing left to discover about the shape of the problem, only a window left to assemble around it.

What changed afterward is where I go looking, not how much there is to find. I still catch myself opening an asset to check whether I already made it. At least now there’s one window that can answer that, instead of a Project window search that only ever confirms the type.

References