← Back to devlog
Engineering · June 2026

Unity's icon support for SO's and MonoBehaviours is broken. I made it work for me.

Every custom script in my project wore the same icons. That sad, beige sheet of paper or blue box Unity hands every MonoBehaviour and ScriptableObject. A hundred-plus types, all identical in the Inspector, the Project window, the Hierarchy. It reads as unfinished, like a kitchen with no labels on anything. I’m making a codebase that outlives the project I’m currently working on. I have to do better than that.

Unity does have a “solution”. Click a script asset, find the little icon dropdown in the top corner of the Inspector, pick an image. Then do that again for the next file. And the next one.

Two problems with that.

The first is obvious. It’s a manual chore per source file, and nobody is going to keep it up across a growing framework. The second is worse: it breaks the moment you use inheritance. Set a nice icon on a base class, write a subclass, and the subclass is right back to the dumb default icon. The icon lives in that one file’s .cs.meta, and a subclass has its own meta that knows nothing about its parent.

How the pros actually do it

So I went looking. Cinemachine’s scripts all have crisp, proper icons, so I cracked open the installed package expecting to find some clever attribute I could copy.

There’s nothing in the code. You can’t tell from the compiled DLL at all. The icon is just a line baked into each script’s .cs.meta:

# CinemachineConfiner2D.cs.meta
MonoImporter:
  icon: {fileID: 2800000, guid: 3327a326efc0c4079a8eada111b11d29, type: 3}

The meta is the source of truth, not the code. They picked the icons once, by hand or by some build step, and froze the result into the metas. Which means a polished, first-party Unity package has the exact same inheritance gap I do. It just has fewer subclasses to care about.

I didn’t want to click dropdowns, and I didn’t want to freeze anything by hand. I like to solve problems. Once.

That’s [CmIcon].

The design

You put one attribute on a type:

[CmIcon("dialogue")]
public class DialogueController : MonoBehaviour { }

An editor scan walks every MonoBehaviour and ScriptableObject type, finds the nearest ancestor carrying a [CmIcon], and writes that icon into the script’s meta. The declaration lives in one place. Every subclass picks it up for free, unless it declares its own.

That’s the whole trick. The same committed-meta result Cinemachine ships, except the assignment is one attribute instead of a manual pass, and it understands inheritance, which theirs doesn’t.

The design calls

MonoImporter.SetIcon, not custom drawing. There’s another way to do this that I considered. Hook the Project and Hierarchy windows and draw the icon yourself at render time. That gets you inheritance for free, yet it can’t touch the Inspector header or the Add Component menu, and it repaints every row every frame. SetIcon writes the native meta reference, so Unity draws it everywhere itself, the header included. The scan is what buys back the inheritance the GUI-hook approach would have given me.

Diff-gated, so it’s idempotent. Writing a meta triggers a reimport, so a naive scan would thrash the entire project on every domain reload. The scan compares the resolved icon to what’s already on the meta and skips when they match. First run writes. Every run after is a no-op. Git only ever sees a diff when an attribute genuinely changed.

The icons are committed, on purpose. They live as real .cs.meta changes in the repo. That’s a feature, not a leak. A teammate pulls the project and sees every icon with no scan, no setup, nothing to run. Deterministic, reviewable, exactly the way the shipped packages do it.

It lives in the basement. New CodiushMaximush.EditorTools module, sitting below the rest of the framework with no dependency back up into it, so the whole thing lifts out into its own package the day I decide it should.

Where it stands

The first thing I pointed it at was my entire dialogue package. Nine scripts, a bunch of SO types with instances sprinkled all over the project. 2 minutes later? All wearing GfxKit generated icons:

TypeIconReads as
SimpleDialogueAsset Speech-bubble icon a speech bubble — the dialogue itself
SimpleDialogueMessage Speech bubble with a single line one line inside it
SimpleDialogueSpeakerAsset Profile-picture icon an empty profile picture — who's talking
SimpleDialogueControllerIDAsset Key icon a key — the stable token that names a controller
SimpleDialogueControllerWrapper Controller badge icon the controller it registers and drives

Keys look like keys, speakers look like people, dialogue looks like talking — and any subclass picks up its parent’s icon from the same scan, no second attribute, no second decision. My custom assets and scripts populate beautifully in the inspector and project view like they’re the highly developed, functional pieces that they are in my brain.

The sad, default icons are gone.

One attribute. Every subclass. Zero dropdowns.