Skip to content

MenuManager.cs

概要

本ツールの中核となるエディター拡張スクリプトです。VRChatのラジアルメニューをUnityエディター上で再現し、直感的な操作でメニュー階層を編集するインターフェースを提供します。

実装は責務ごとに複数のファイルへ partial class として分割されています。

ファイル役割
MenuManager.csウィンドウ本体・状態管理・エントリーポイント
MenuManager.DrawUI.cs描画処理全般(ラジアルUI・ツールバー・詳細パネル)
MenuManager.TreeBuilder.csメニューツリーの構築・レイアウト復元
MenuManager.Serialization.csレイアウト保存・シリアライズ
MenuManager.InputHandler.csクリック・ドラッグ・ドロップ・リネーム入力処理
MenuManager.Operations.csナビゲーション・並び替え・フォルダ作成などの操作
MenuManager.Inventory.csインベントリパネル描画・ドラッグ操作
MenuManager.Helpers.cs幾何計算・アイコンキャッシュ・ユーティリティ

主な役割と機能

  • レイアウトの描画

スライス描画による円形メニューUI。

  • MA統合ツリー生成

通常の VRCExpressionsMenu に加え、アバター内に散らばった MA Menu Installer コンポーネントを探索して仮想的な統合メニューツリーを合成。

  • 同一階層上限のエミュレート

8項目を超過した要素に対して、VRChatゲーム内と同様自動的に ...(More) フォルダを生成して収納・展開します。

  • 永続化

スライスの並び替えやフォルダ作成・挿入といった配置情報を生成し、MenuLayoutData へのセーブを仲介。

実装詳細

メニューの描画アルゴリズム

VRChatのラジアルメニューを模倣するため、Handles.DrawSolidArc などを多用して扇形を動的描画します。 通常時は8分割ですが、VRC Style UI時に項目が8個を下回る場合は自動的にスケールを調整します。

csharp
private void DrawSlice(Rect rect, Vector2 center, float startAngle, float sweepAngle, float radiusInner, float radiusOuter, Color bgColor, bool isHover, bool isMore){
    Handles.color = bgColor;
    Handles.DrawSolidArc(center, Vector3.forward, Vector3.right, 360f, radiusOuter);
    
    Vector2 dir = new Vector2(Mathf.Cos(startAngle * Mathf.Deg2Rad), Mathf.Sin(startAngle * Mathf.Deg2Rad));
    Vector2 p1 = center + dir * radiusInner;
    Vector2 p2 = center + dir * radiusOuter;
    
    Handles.color = _colors["SEPARATOR"];
    Handles.DrawLine(p1, p2, 2.0f);
}

メニューツリーの再帰的構築とMA結合

このツールは1つのMenuを展開するだけでなく、Modular Avatarの分散構造を結合します。

csharp
private void RebuildMenu(){
    //標準ルートメニューのロード
    var rootObj = _descriptor.expressionsMenu;
    _menuTreeRoot = BuildMenuNode(rootObj, "");

    //Modular Avatar Menu Installer の検索と統合
    var installers = _targetAvatar.GetComponentsInChildren<ModularAvatarMenuInstaller>(true);
    foreach (var installer in installers){
        if (installer.menuToAppend != null){ //パスの文字列をパースし、_menuTreeRootの階層に仮想的につなぎ合わせる
            string targetPath = installer.installPath;
            AddInstallerEntries(_menuTreeRoot, installer.menuToAppend, targetPath);
        }
    }
    
    ApplyLayoutToTree(_menuTreeRoot, layoutData); //過去に保存したMenuLayoutDataとのすり合わせ
    ReevaluateOverflow(_menuTreeRoot); //超過項目の自動生成
}

項目上限数のエミュレート

VRChatのラジアルメニューは1フォルダにつき8スロット固定です。9項目以上ある場合は、自動的にオーバーフロー再起が行われます。 MenuManager はこれをシミュレートし右下の手前部分に固定の ...(More) フォルダを生成して隔離します。

csharp
private void ApplyOverflowMore(MenuNode node) {
    const int MAX_CONTROLS = 8;
    
    if (node.Children.Count <= MAX_CONTROLS) return;

    // 最初の7枠分までをこの階層に残す
    var keptChildren = node.Children.Take(MAX_CONTROLS - 1).ToList();
    var overflowing = node.Children.Skip(MAX_CONTROLS - 1).ToList();

    // 仮想の子フォルダ [More] ノードを作成
    var moreNode = new MenuNode {
        Name = "…(More)",
        IsVirtualFolder = true,
        Icon = BuiltinMoreIcon
    };

    moreNode.Children.AddRange(overflowing);
    node.Children = keptChildren;
    node.Children.Add(moreNode); // 8番目の要素として追加

    // 超過分がさらに8を超える場合、再帰的に超過フォルダの中に超過フォルダを作る
    ApplyOverflowMore(moreNode);
}