codezoom
v1.2.0
Published
Walk the architecture of a Unity or .NET C# project one type at a time: a UML view centred on one Focus, its Dependents above and Dependencies below, in your browser or through MCP tools for coding agents.
Maintainers
Readme
codezoom
Walk the architecture of a C# codebase (a Unity project or a .NET solution) one type at a time. codezoom indexes your code and opens a page in your browser with a UML view centred on one type, the Focus: the types that depend on it (Dependents) above, the types it depends on (Dependencies) below. Click a box to Drill into it, go Back along your Trail, or Jump anywhere by name. In a Unity project the scenes, prefabs and ScriptableObject assets wired to your code are nodes too. Coding agents can ask the same questions through MCP.
New in 1.2.0: Unity assets as nodes. 1.1.0 brought agent
access (MCP and --json) and
plain .NET C# projects.
Overview, demo and design notes: https://jacobcohenisrael.github.io/grill-and-drill/
Install and run
From the root of a Unity project (the folder with Assets/) or a .NET
solution (the folder with the .sln):
npx codezoomor install it once and name a type to start from:
npm install -g codezoom
codezoom PlayerControllerIt indexes your own code, serves the page on 127.0.0.1 (port 4777, or the
next free one) and opens your browser. Nothing leaves your machine.
Requirements: Node.js 22.23 or newer (older versions crash inside
tree-sitter's native binding). v1 is made for macOS ("Open in
Rider" and the browser launch are macOS-first; elsewhere, open the printed
address yourself). In a Unity project, exact results come from the
.csproj files Unity writes when Rider or Visual Studio is its external
editor; without them (a fresh clone) codezoom reads .asmdef files and
Unity's folder rules instead. In a .NET solution, exact packages come from
dotnet restore (see Plain .NET); no .NET SDK is needed to run
codezoom.
On a clean install npm 11 may print npm warn install-scripts 2 packages have
install scripts not yet covered by allowScripts, naming tree-sitter and
tree-sitter-c-sharp. That is expected and harmless: both ship prebuilt
binaries for macOS, Linux and Windows (x64 and arm64), and codezoom loads
those, so there is no need to allow the scripts.
The first run: what is your code
The first run in a project opens the setup page. It shows the Unity
version and every assembly, grouped by its top folder under Assets/ or
Packages/ (biggest first), each with its file count and the namespace
roots its files declare, so a vendor SDK stands out next to your own code.
Pre-ticks follow the default rules: assemblies with files under Assets/,
minus test assemblies; vendor folders (Plugins, ThirdParty at any
depth; TextMesh Pro, Photon and the like right under Assets/) stay
out. One click ticks or unticks a whole folder group; opening an assembly
shows its folders, so a vendor folder inside your own assembly can be left
out. Save and index writes codezoom.config.json:
{
"assemblies": ["MyGame.Core", "MyGame.Runtime", "MyGame.Editor"],
"excluded": ["Assembly-CSharp-firstpass", "MyGame.Tests", "SomeVendorSDK"],
"excludeFolders": ["Assets/SomeVendorSDK"],
"includeFolders": ["Assets/Plugins/MyOwnPlugin"]
}Commit it: it is how your team shares the choice. An assembly in neither
list is new; it follows the default rules and the top bar offers "N new
assemblies: review". For folders, the most specific rule wins. Reopen the
page any time with codezoom setup or the Setup link.
In a .NET solution the same page lists projects instead, grouped by the
folder they sit in (src/, tests/). Every project in the .sln starts
ticked except test projects, and the config lists them under the same keys.
Plain .NET
codezoom reads SDK-style projects the way the .NET SDK does:
- Projects. It reads every project in the
.slnor.slnxfiles at the root, or every.csprojunder the folder when there is no solution. A project they reference but don't list is read as someone else's code. - Own code. Every project except test projects: those with
IsTestProject, a reference to xUnit, NUnit, MSTest orMicrosoft.NET.Test.Sdk, or the MSTest SDK. - Sources. The SDK's default
**/*.csminusbin/andobj/, changed by the project's<Compile Include>and<Compile Remove>items. It also reads the nearestDirectory.Build.props,<ImplicitUsings>,<Using>items, and top-level statements (as theProgramclass). - Foreign libraries. Each NuGet package is one node. A type is matched
to its package through the DLLs
dotnet restorerecorded inobj/project.assets.json. ASP.NET Core's shared framework is one node too. The .NET base library stays hidden. - Not restored. Without a restore, a package is matched by its root
namespace (
Newtonsoft.Json, orMicrosoft.Extensions.LoggingforMicrosoft.Extensions.Logging.Abstractions). Every Screen and every agent answer then says thatdotnet restorewould make it exact. A restore can move a type to the package it really comes from.
The base library's type names come from an installed SDK's targeting pack. Without one they come from a list of .NET 8's types that ships with codezoom.
Moving around
- Drill: click a box. The view dives into it until it is the new Focus.
- Back: click the previous Focus (marked ↩), press Backspace, or use the browser's Back button. Every Focus has its own address.
- Jump: type in the search box (
/). It matches substrings and word starts:CA,CampApandplicaall findCampaignApplication. - Boxes are sized by Weight, how heavily one end leans on the other (√ of References × the members they sit in): large, medium or small within the row, large first. Foreign libraries (UnityEngine, each package) are one box each; .NET is hidden.
- Edges are drawn the UML way: Inherits and Implements with a hollow triangle, Has and Uses with an open arrow, dashed for Implements and Uses.
- Hover a box for the References behind its edge (member and file:line). Each row has a filter.
- Click the Focus for its source (one tab per partial file); Open in Rider or a line number opens the file there. Esc closes it.
- Show animations in the top bar turns the zoom off: Drill and Back then cut straight to the new screen, as Jump does, and nothing on the page moves on its own. It starts off when your system asks for reduced motion; the choice you make in the toggle wins and is remembered in the browser.
Unity assets
Scenes (.unity), prefabs (.prefab) and ScriptableObject assets
(.asset) in your own folders are nodes when they are wired to your code:
an asset stays if it has an edge to one of your types, or to another asset
that stays. Art-only prefabs drop out. Assets in vendor or excluded folders
that yours point at roll into that folder's foreign library, like vendor
code. Each file is one node, drawn as a UML artifact (a page with a folded
corner) and labelled «scene», «prefab» or the ScriptableObject's type.
Edges start at the asset and never add an edge between two types, so a type's Dependencies stay about code and its asset users show up among its Dependents. Strongest first:
| Edge | From | To | |---|---|---| | Inherits | a prefab variant | its base prefab | | Instance of | a ScriptableObject asset | its type | | Contains | a scene or prefab | the type of a component (its script), or a prefab nested in it | | Assigns | any asset | an asset set in a serialized field (overrides in a nested prefab count for the outer asset) | | Calls | a scene or prefab | the type whose method one of its UnityEvents calls |
Weight is the same √(R × M): R counts components, assignments and calls, M the GameObjects they sit in (a ScriptableObject asset counts as one). In a row, assets come after code types of the same size, nearest folder first.
- The Focus box of a scene or prefab lists its component types with counts,
yours first (
Tower ×3, Collider2D ×4); a ScriptableObject asset's lists the fields that assign assets (enemyPrefab = Goblin.prefab). - Click an asset Focus for its outline instead of YAML: the GameObject tree with each one's components (yours highlighted), assigned assets and UnityEvent calls, each a link to Drill to. Hovering a neighbour marks the GameObjects behind its edge. Reveal in Finder shows the file.
- Search finds assets by file name (
Goblin.prefab). - Show assets in the top bar takes them out of the rows and search; the choice is remembered in the browser.
Not seen: references made by string (Resources.Load, Addressables keys),
and materials, textures, audio, animations and animator controllers, which
are not nodes.
Golden walks
A golden walk is a written-down sequence of moves through a project you know, with the neighbours each screen must (and must not) show. Keep them next to the project and replay them after changes:
codezoom walk walks/*.walk.json{
"name": "Towers and their config",
"start": "TinyTowers.Towers.Tower",
"expect": {
"dependents": ["BuildGrid", { "name": "TowerConfig", "cycle": true }],
"dependencies": [
{ "name": "ITargetable", "edge": "has", "size": "large" },
{ "name": "UnityEngine", "size": "small" }
],
"absent": { "dependencies": ["WaveSpawner"] }
},
"steps": [
{ "drill": "TowerConfig", "expect": { "focus": "TowerConfig" } },
{ "back": true, "expect": { "focus": "Tower" } },
{ "jump": "WSp", "expect": { "focus": "WaveSpawner" } }
]
}startnames the first Focus the way the command line does (one node).- A step is
drill(a neighbour on the current screen),back, orjump(the first search result), each with an optionalexpectfor the screen it lands on. - Listed neighbours must be in that row;
absentones must not. Edge kind (inherits,implements,has,uses),size(small,medium,large) andcycleare checked only where given. Anything unlisted is allowed. Names match a node's full name, shown name or simple name. - It prints a line per screen with what differs, and exits 0 when every screen is as expected, 1 when one differs, 2 when a walk can't be read.
For coding agents
An agent reads the same Screens through three MCP tools. Add them to a project for Claude Code with:
claude mcp add codezoom --scope project -- npx -y codezoom mcpThat writes a .mcp.json you can commit, so the whole team gets them. Any
MCP client works the same way: it runs codezoom mcp in the project root
and talks over stdio.
| Tool | Answers |
|---|---|
| search(query, limit?, kind?, assets?) | Nodes that match, as Jump matches them: name, full name, kind (a type's, scene, prefab, asset or library), label, and the Dependents and Dependencies counts. kind narrows the results (type means any of your types). |
| screen(focus, filter?, limit?, offset?, assets?) | The Focus box (label, public and serialized members, how many private ones; an asset's components or asset fields) and both rows in the page's order. Each neighbour has its edge kind, Weight with R and M, size, cycle mark, namespace (or folder) distance and declaring file. 25 per row by default; each row gives its total ("25 of 472"). |
| references(from, to, limit?, offset?) | The References behind one edge: file:line and the member they sit in (for an asset, the GameObject path), 100 at a time. |
assets: false leaves scenes, prefabs and ScriptableObject assets out, as
the page's Show assets toggle does. The tools keep no Trail: to Drill, call
screen on a neighbour. None of
them returns source, since the agent can open the file:line itself. They use
the page's config, cache and file watching. Without a codezoom.config.json,
every answer says that the default rules chose own code and suggests
codezoom setup.
The same questions work from a shell, as text, or with --json as the
tools' own JSON:
codezoom search WSp --json
codezoom screen Tower --limit 10 --json
codezoom references Tower TowerConfig --jsonCommands and options
| | |
|---|---|
| codezoom [Type] | Index and open the page at Type (or the start page). |
| codezoom setup | Open the setup page. |
| codezoom walk FILE... | Replay golden walks; no server. |
| codezoom mcp | Serve the agent tools over stdio; no browser. |
| codezoom search QUERY | Nodes that match, best first. |
| codezoom screen NODE | One Screen: the Focus box and both rows. |
| codezoom references FROM TO | The References behind one edge. |
| --json | Answer search, screen or references as JSON. |
| --filter TEXT, --limit N, --offset N | Filter and page the rows of a screen; search takes --limit, references --limit and --offset. |
| --kind KIND, --no-assets | Narrow search to one kind; leave assets out of search and screen. |
| --port N | First port to try (default 4777). |
| --config FILE | Use a config kept outside the project. |
| --cache-dir DIR, --no-cache | Where the index cache lives, or none. |
| --no-watch | Don't watch files while open. |
| --no-open | Don't start a browser; print the address. |
| --report | Also list names that could not be resolved. |
| --foreign-types FILE | Read foreign type names from a list instead of Unity's DLLs or a restore (tests). |
Cache and watching. What each file gave is cached in .codezoom/ at the
project root (it holds its own .gitignore), so a relaunch re-reads only
changed files. While open (the page or codezoom mcp), codezoom watches
Assets/, Packages/ and the .csproj files (scripts, scenes, prefabs,
.asset files and their .meta files), or in a .NET solution its sources,
project files and restores. After a change the page shows a notice
instead of reshuffling the screen you are on. The cache and the config you
save are the only files codezoom writes into a project.
How it reads C#
- tree-sitter-c-sharp plus its own name resolution: namespaces,
usingdirectives (global and implicit ones too) and aliases, nested types, type parameters. Partial types merge; a generic type is one node; both sides of every#ifare read (a file whose#ifsplits a declaration is read once per configuration). - A field or property type is Has, and so is the type of a primary constructor parameter, since the class keeps it like a field.
- Foreign libraries come from the type tables of the DLLs your projects reference: Unity's (from an installed editor when the project's version isn't installed), or the restored NuGet packages'.
- Unity assets are read from their text serialization (Force Text, Unity's
default): documents, fileIDs, the Transform tree for GameObject paths and
{fileID, guid}references. A script's GUID leads through its.metafile to the type its file is named after, as Unity requires; a script in a DLL is found by the fileID Unity derives from its name. - Not seen: the type of a
varlocal, and extension-method calls.
Developing
git clone https://github.com/JacobCohenIsrael/grill-and-drill.git
cd grill-and-drill/codezoom
npm install
npm run build
npm link # puts codezoom on your PATH
npm test # golden walks, MCP and unit tests; no Unity or .NET SDK neededThe tests index examples/TinyTowers (Unity, with a made-up scene, prefabs
and ScriptableObject assets written as text) and examples/TinyShop (.NET)
with committed lists of the foreign types each one uses (test/fixtures),
so they pass with or without Unity or a .NET SDK. After
changing an example, refresh its list. For TinyTowers, read it from an
installed editor: node scripts/extract-foreign-types.mjs ../examples/TinyTowers
test/fixtures/tinytowers-foreign-types.json. For TinyShop, read it from a
restored copy: dotnet restore, then run the same script on that copy with
test/fixtures/tinyshop-foreign-types.json. The base library lists in
data/ come from an SDK's targeting packs:
node scripts/extract-base-library.mjs <dotnet root> net8.0. Design terms are in
GLOSSARY.md,
decisions in docs/adr.
License
MIT
Credits
Inspired by LIVE: Uncle Bob on Software Fundamentals in the Age of AI on Matt Pocock's channel. The idea grew from Robert C. Martin's uml-viewer, and codezoom was designed with Matt Pocock's grill-with-docs skills.
