Unityでの作業が快適に!シーン上でメニューを開けるContextCircleMenuの紹介

2024/04/28に公開

はじめに

Unity 2023.2 からScene上でコンテキストメニューを開けるようになったそうです。

https://docs.unity3d.com/2023.2/Documentation/Manual/SceneViewContextMenu.html

Unity Japanさんからも紹介されています。

https://x.com/unity_japan/status/1777894275633795215

便利そうですね。

自分でも使ってみました。

image.png

かなりよさそうです。
これを使えば開発効率爆上がりに違いありません...!!

...

.....

.........

......................

本当にそうか

そうなのか、Unity??

これ、追加しすぎたら見切れちゃまいすよね...?

Assets > Create のメニューなんかは見切れてしまうことが多いです。

それに四角いし、縦に羅列されているせいか瞬時に情報が頭に入ってこないため、選択することも大変です。

ということで、これを解決するために新たなコンテキストメニューを開発しました。

その名も 「ContextCircleMenu」

今回はこのライブラリを紹介していこうと思います。

Context Circle Menu

まずはリポジトリ

https://github.com/Garume/ContextCircleMenu

以下のようなメニューを作成することができます。

Image from Gyazo

実際に使ってみるとこんな感じ。

Image from Gyazo

ボタンが円状に配置されるため、どこになにがあるか一瞬で把握できます。
また、それぞれまでの距離が一定であるため最小のマウス移動でクリックすることができます。

ちなみに、このメニューはBlenderを参考にしています。

image.png

デモ

ContextCircleMenuの簡単な使用方法を説明していきます。

要求

  • Unity 2022.3 以上

インストール

  1. Window > Package ManagerからPackage Managerを開く
  2. 「+」ボタン > Add package from git URL
  3. 以下を入力する
https://github.com/Garume/ContextCircleMenu.git?path=/Assets/ContextCircleMenu

メニューの追加方法

メソッドにCircleContextMenuアトリビュートをつけるだけで追加することができます。

using ContextCircleMenu.Editor;
using UnityEngine;

public static class Menu 
{
    [ContextCircleMenu("Debug")]
    public static void DebugTest()
    {
        Debug.Log("Debug");
    }
}

Image from Gyazo

フォルダー分け

フォルダー分けしたいときは/で区切ってください。

using ContextCircleMenu.Editor;
using UnityEngine;

public static class Menu 
{
-    [ContextCircleMenu("Debug")]
+    [ContextCircleMenu("Example/Debug")]
     public static void DebugTest()
     {
         Debug.Log("Debug");
     }
}

Image from Gyazo

メニューの開閉

シーン上でAキーを押下することでメニューを開くことができます。

開閉キーのカスタマイズ

デフォルトの開閉キーはAキーですが、好きなキーに変更することができます。

まずEdit > Shortcutsでウィンドウを開いてください。

Context Circle Menuと検索すると、下図のような設定が見つかります。

image.png

ここで好きなキーに設定してください。
Shift + Aなども設定できます。

以上がContextCircleMenuの簡単な使用方法になります。

設計

使用方法だけ説明してもあれなので、本ライブラリを開発するうえで意識した設計部分を話そうかなと思います。
大した事はやっていないので読み飛ばしてもらってもかまいません。

ちょっと命名がわかりづらいので以下に固定します。

  • ContextCircleMenuはAキーで開くものの本体
  • CircleMenuはAキーで開いたメニュー (ボタンの集合)
  • Buttonはクリックできるもの

ContextCicleMenuの分離

ContextCicleMenuは単体で動くように設計されています。
今回の場合はScene上で動かすことを売りにしていますが、Scene上以外でも動かすことができます。

実際に内部処理では、「ContextCircleMenuをContextCircleMenuLoaderで使用しScene上にて動かす。」といったことを行っています。

ContextCiecleMenuはVisualElementを継承しています。
そのためVisualElementがRootである、Inspector,Hierarchy,EditorWindowといったUnity Editor上のあるゆるウィンドウに簡単に追加することができます。

この設計は以前開発・公開したUniAquariumと同様です。

https://github.com/Garume/UniAquarium?tab=readme-ov-file#aquarium-component

UniAquariumは、「UnityEditor上で水族館を模したWindowを開く」という機能を提供しますが、「水族館を模したコンポーネント(VisualElement)」も同時に提供しています。

そのため上記のリンク先と同様の手順でContextCircleMenuをHierarchy Windowに追加することが可能です。(円状にボタンが広がる都合でただ追加するだけでは動かないと思いますが...)

また、この設計はVisualElementがノード状(HTMLのDom的な?)に構築されて描画されることに即していると思っています。

ノード構造のCircleMenu

ContextCircleMenuはCicleMenuというノード構造のクラスをただ1つ持っています。
このCircleMenuはノードであるため同じ型の1つの親、複数の子を所持しています。(通常はRight,Leftで親はいないかも)
ContextCircleMenuから指示があるたびに、所持しているCircleMenuをもとに親、子のどれかに切り替わります。

public abstract class CircleMenu
{
    protected internal readonly List<CircleMenu> Children = new();
    protected internal readonly CircleMenu Parent;
    ...
}
ContextCircleMenuから「戻る」指示が来た場合
現在の所持しているCircleMenu → CircleMenu.Perent に再代入

ContextCircleMenuから「○○を開く」指示が来た場合
現在の所持しているCircleMenu → ○○に再代入 

このように切り替えたのちに、再描画が行われます。
再描画時にはCircleMenuのCreateElements()を呼び描画すべきVisualElement[]がContextCicleMenuに伝えられます。これらをAdd()してMarkDirtyRepaintを呼べば再描画完了です。

こうすることで、ContextCircleMenuはメニュー情報をすべて持つ必要はなく、ただ1つのCicleMenuを持てばよく、CicleMenuは自分自身と関係があるものと描画物にだけ集中すればよく、まあまあ疎結合になっていると思います。

ただ、ここで問題になってくることが、フォルダーをどうするかという点です。フォルダーは親のメニューに戻る Back()子のメニューを開く Open()という処理が必要になってきます。
さらに、実際にこれらの処理をたたくのはCircleMenuではなくCircleMenuにより生成されるButton(VisualElement)です。
自然に考えてこれらのメソッドBack() Open()は、本体のContextCircleMenuに持たせたいところです。
しかしそうしようとすると、下位の存在(Button)が上位の存在(ContextCircleMenu)に依存することになります。どうにか依存性の逆転を図りたいところです。

そこで、メニュー構築時にメソッドOpen()を渡すことにしました。

メニューの構築

先ほども話した通りCircleMenuはノード構造で存在するので、あらかじめ初期化(メニューの構築)をしなければなりません。

また、メニューはいろいろな方法、いろいろな情報から作成できるようにしたいわけです。

  • Attribute(属性)
  • 手動
  • ボタンのスタイルの変更
    などなど。

そこで以下のように追加できるようにしました。
CircleMenuBuilderを用いることでメニューを感覚的に構築できるようにしています。

ContextCircleMenu.CreateMenu(builder =>
{
    builder.AddMenu("Custom/Debug Test", new GUIContent(), () => Debug.Log("custom/test"));
    builder.AddMenu("Debug Test", new GUIContent(), () => Debug.Log("test"));
});

内部ではICircleMenuFactoryを用いてCircleMenuを作成・構築しています。そのため、Factoryを差し替えることでボタンのカスタマイズが可能です。(ボタンだけでなくメニューの作成も考えなければならないため面倒ではある)
本当はButtonFactory的なのを差し替えてボタンのみを変更できればとも思ったのですがそれだとすべてのボタンのスタイルが変わってしまうので今回はなしとしました。(変更するかも)

メニュー構築時にメソッドOpen()を渡す

上で追加された情報はBuild()され、構築されます。

internal CircleMenu Build(IMenuControllable menu)
{
    _rootFactory ??= new RootMenuFactory();
    _folderFactory ??= new FolderMenuFactory();

    var root = _rootFactory.Create();
    foreach (var factory in _factories)
    {
        var pathSegments = factory.PathSegments.SkipLast(1);
        var currentMenu = root;
        foreach (var pathSegment in pathSegments)
        {
            var child = currentMenu.Children.Find(m => m.Path == pathSegment);
            if (child == null)
            {
                child = _folderFactory.Create(pathSegment, menu, currentMenu);
                currentMenu.Children.Add(child);
            }

            currentMenu = child;
        }

        currentMenu.Children.Add(factory.Create());
    }

    return root;
}

_factoriesはAddMenu()から追加されていきます。

少し長いですが、やっていることは単純でパスに/があればFolderCircleMenuを追加して最後にfactoryから作成されるCicleMenuを追加しています。

メソッドOpen()を渡している部分は以下です。

child = _folderFactory.Create(pathSegment, menu, currentMenu);

menuはIMenuControllableであり、

public interface IMenuControllable
{
    public void Show();
    public void Hide();
    public void Open(CircleMenu menu);
    public void Back();
}

Open()を持っています。

この流れでCircleMenuにOpen()メソッドを渡すことに成功しました。

正直Build()にIMenuControllableというインターフェースが必要なことは意味不明ですが、ContextCircleMenuを渡したりメソッドを直接渡すよりかはましな気もします。

もっと良い設計はあるはずで、そもそも根本的に違う選択肢、「CircleMenuにVisualElementを継承させておき、最初にすべて描画、Show() Hide()で切り替える」であったり、「CircleMenuのデータ構造を変える」など考え始めたらきりがないです。そのため、今回の設計を折衷案として一旦公開することにしました。

以上で設計の話は終わりにします。

終わりに

シーン上でコンテキストメニューを開ける「ContextCircleMenu」を紹介しました。

このツールで一歩先に進めたかどうかは正直わかりませんが、Unityでの作業が快適になることは間違いなしだと思います。

よろしければ🌟Star🌟いただけると嬉しいです。

https://github.com/Garume/ContextCircleMenu/tree/master

また、フィードバックもいただけると幸いです。

ありがとうございました。

Discussion