[Unity] Project View に色をつけてアセットを探しやすくするエディタ拡張

2020-11-15

この記事は

  • Unity の Project View のフォルダに、アセットの種類ごとに色をつけて目当てのものを探しやすくするエディタ拡張を作った
  • そこに至る経緯とエディタ拡張のコードを紹介する

フォルダが増えると探しづらい

普段、個人制作のゲームを Unity で開発する際に自分は以下のようなレイアウトで作業している:

で、これはゲーム開発あるあるだと思うが、ある程度プロジェクトの規模が大きくなってくると アセットの数・フォルダの数が増えてきて作業対象のアセットが見つけづらくなる、というのがある。

Unity では Prefab や Material を行き来して作業するようなことがよくあるので、 Project View からフォルダを辿ってアセットを選択する、というのは割とよくやる。 そのたびに「ええと、Material のフォルダはどこだっけ…」と探すのは地味に認知コストが高かった。 何かもっと、パッと見で目的のアセットを見つけやすくしたいと思っていた。

Asset Store に色やアイコンをつけるアセットもありそうだが、 自分用に調整したものが欲しかったのと、ちょっとした実装で作れそうなレベルだったので自分でエディタ拡張を自作した。

フォルダ名を見て種類ごとに固有の色をつける

色々試して最終的に自分が一番良いなと思ったのは、

  • Material は緑、Script は青、といったように種類ごとに固有の色をつける

というものだ。以下のような見た目になる:

まず前提として、自分は以下のようなルールで Unity プロジェクトのアセットフォルダを整理している:

Assets/
|---00_Framework/
|
|---01_GameSpecific/
|   |---Materials/
|   |---Prefabs/
|   |---Scripts/
|   ...
|
|---02_Workflow/
|
|---外部アセット_1/
|---外部アセット_2/
|---外部アセット_3/
:
  • Asset Store からインポートするファイルは Assets/ 直下にフォルダが作られるものが一般的なので、 それはそのまま Assets/ 直下に置く
  • 自分で作るものは namespace ごとにフォルダを切るイメージ
    • 共通ライブラリ、ゲーム固有アセット、レベルエディタみたいな開発だけで使うやつ、など
    • 00_ などの prefix をつけて自分で作ったものが上にソートされるようにしておくと見つけやすい
  • フォルダは Materials/, Prefabs/ などアセットの種類ごとに切る
    • (外部アセットなどを見ていても、一般的な分け方かと思う)

で、Assets/ 直下のフォルダには特定の色(黄色)をつけて、 それ以後はフォルダ名を見て 「”material” で始まっていたら緑」 といった単純な規約で色をつけてしまう。

色を固定してしまうことで、「Material フォルダ以下の Avatar フォルダを探す」といった場合に まず緑色のところを見つけてそこからフォルダ名を読む、といった手順で探すことができるので、 最初にフィルタリングができて目的のファイルが見つけやすくなるのだ。


個人的にはこの拡張を入れてから、明らかに作業が快適になったことを実感できた。 ささいなことではあるが、ゲーム開発は細かい作業の積み重ねなので こうした小さな改善はトータルで見ると作業効率やモチベーション維持に効いてくると思う。

他の人の反応

先日、仕事のビデオチャット会議で画面共有をして自分の Unity の画面を見せることがあったのだが、 メンバーの一人が「え、その色ついてるやつ何」と反応した。

説明すると「それは便利だ、うちにも欲しい」と共感してもらえた。 その人も同じような認知コストを感じていたようだ。 ( 「Photoshop のレイヤーに色つけるみたいな感じで Unity でも色をつけたかった」 と言っていた)

ということで、この拡張は仕事のプロジェクトにも導入することにした。
(※ 任意で ON / OFF できるようになっているので使いたい人だけが使える形になっている)

ちなみにここに至るまでの経緯

実は 1 年前にも似たような記事を書いていて、その時は階層ごとに色をつけるアルゴリズムでやっていた:

最初はこれで十分かなと思っていたのだが、 作業をしていて「ええと Material フォルダは…」と結局文字を読んでいる自分に気づいた。 色によるフィルタリングがあまり働いていなかったのだ。

階層で色をつけてもチカチカするだけで微妙かな、と思ったので 第 1 階層のフォルダでざっくり色付けするのもやってみたが、これもまあ気休めにしかならなかった。

で、最終的に今回のやり方に辿り着いたというわけだ:

ソースコードと使い方

#if UNITY_EDITOR
using System.IO;
using UnityEditor;
using UnityEngine;

namespace AltoLib
{
    public static class ColoredProjectView3
    {
        const string MenuPath = "Alto/ColoredProjectView (Kind)";
        static readonly string[] Keywords = {
            "scene", "material", "editor", "resource", "prefab", "shader", "script"
        };

        [MenuItem(MenuPath)]
        static void ToggleEnabled()
        {
            Menu.SetChecked(MenuPath, !Menu.GetChecked(MenuPath));
        }

        [InitializeOnLoadMethod]
        static void SetEvent()
        {
            EditorApplication.projectWindowItemOnGUI += OnGUI;
        }

        static void OnGUI(string guid, Rect selectionRect)
        {
            if (!Menu.GetChecked(MenuPath))
            {
                return;
            }

            var assetPath = AssetDatabase.GUIDToAssetPath(guid);
            var pathLevel = CountWord(assetPath, "/");

            var originalColor = GUI.color;
            GUI.color = GetColor(pathLevel, assetPath);
            GUI.Box(selectionRect, string.Empty);
            GUI.color = originalColor;
        }

        static int CountWord(string source, string word)
        {
            return source.Length - source.Replace(word, "").Length;
        }

        static Color GetColor(int pathLevel, string assetPath)
        {
            var fileName = Path.GetFileName(assetPath);
            string[] folderNames = assetPath.Split('/');

            int id, depth;
            (id, depth) = GetColorIdAndDepth(pathLevel, assetPath);

            Color color = (EditorGUIUtility.isProSkin)
                ? GetColorForDarkSkin(id)
                : GetColorForLightSkin(id);

            float alphaFactor = 1.0f - (depth * 0.25f);
            alphaFactor = Mathf.Clamp(alphaFactor, 0, 1f);
            color.a *= alphaFactor;
            return color;
        }

        static (int id, int depth) GetColorIdAndDepth(int pathLevel, string assetPath)
        {
            if (pathLevel == 1) { return (0, 0); }

            int depthBase = 0;
            string[] folderNames = assetPath.Split('/');
            foreach (string folderName in folderNames)
            {
                var lowerName = folderName.ToLower();
                for (int i = 0; i < Keywords.Length; ++i)
                {
                    if (lowerName.StartsWith(Keywords[i]))
                    {
                        return ((i % 7) + 1, pathLevel - depthBase);
                    }
                }
                ++depthBase;
            }
            return (-1, 0);
        }

        static Color GetColorForDarkSkin(int id)
        {
            switch (id % 8)
            {
                case 0: return new Color(8.4f, 8.4f, 0.0f, 0.45f);
                case 1: return new Color(9.6f, 0.5f, 0.5f, 0.50f);
                case 2: return new Color(0.0f, 9.6f, 0.0f, 0.40f);
                case 3: return new Color(8.4f, 0.0f, 8.4f, 0.50f);
                case 4: return new Color(9.6f, 3.5f, 0.0f, 0.50f);
                case 5: return new Color(0.0f, 4.8f, 9.6f, 0.40f);
                case 6: return new Color(9.6f, 3.0f, 3.0f, 0.50f);
                case 7: return new Color(2.0f, 2.0f, 9.6f, 0.65f);
            }
            return new Color(0, 0, 0, 0);
        }

        static Color GetColorForLightSkin(int id)
        {
            switch (id % 8)
            {
                case 0: return new Color(1.4f, 1.4f, 0.0f, 0.15f);
                case 1: return new Color(1.6f, 0.0f, 0.0f, 0.15f);
                case 2: return new Color(0.0f, 1.6f, 0.0f, 0.15f);
                case 3: return new Color(0.8f, 0.0f, 1.4f, 0.15f);
                case 4: return new Color(1.6f, 0.5f, 0.0f, 0.15f);
                case 5: return new Color(0.0f, 0.8f, 1.6f, 0.15f);
                case 6: return new Color(1.6f, 0.4f, 0.4f, 0.15f);
                case 7: return new Color(0.2f, 0.2f, 1.6f, 0.15f);
            }
            return new Color(0, 0, 0, 0);
        }
    }
}
#endif
  • 単純なコードなので、多少プログラムの心得がある人なら見れば何をやっているかわかるかと思う
  • カスタムしたい場合は単純にソースコードを書き換えてほしい
    • 色をつけるキーワードを変えたい場合はクラス先頭の Keywords を書き換えてもらえばよい
    • 色は GetColorForDarkSkin() (ダークモードじゃない場合は GetColorForLightSkin())の中身を書き換えてもらえばよい

実行環境

  • 自分は 2019.4.1f1 (Unity 2019 の LTS 版)で動作確認を行っている
  • Unity 2019 ではエディタのデザインが微妙に変わったりしていたので、 バージョンが変わると本記事に記載したものと見え方や色味が変わるかもしれない


以上、 「無かったら無かったで何とかするけど、あると助かる」 シリーズでした。