第18章 HierarchySort - エディター拡張入門

第18章 HierarchySort

Hierarchy ウィンドウでは、ゲームオブジェクトの順番を任意に並び替えることが可能です。デフォルトでは uGUI の描画順を決定する Transform Sort となります。また、Preferences ウィンドウにある「Enable Alpha Numeric Sorting」を有効にすると、ゲームオブジェクトをアルファベット順に並び替える Alphabetical Sort を選択できるようになります。

Alphabetical Sort は、Unity4.5までデフォルトだったソート方法

図18.1: Alphabetical Sort は、Unity4.5までデフォルトだったソート方法

ただ、ここで1つ抑えてほしいことは あくまで Hierarchy の表示順を変えるということであり GameObject.Find などのゲームオブジェクトの取得順は変更されていない、ということに注意してください。

本章では、HierarchySort を自分で作成する方法を紹介していきます。

18.1 HierarchySort を自作する

公式のスクリプトリファレンスには サンプルとして、Alphabetical Sort を作成する方法が掲載されています。

HierarchySort を自作するには、BaseHierarchySort を継承したクラスを用意して Compare メソッドをオーバーライドします。

public class AlphaNumericSort : BaseHierarchySort
{
    public override int Compare(GameObject lhs, GameObject rhs)
    {
        if (lhs == rhs) return 0;
        if (lhs == null) return -1;
        if (rhs == null) return 1;

        return EditorUtility.NaturalCompare(lhs.name, rhs.name);
    }
}

18.2 TagSort を作成する

今回は ゲームオブジェクトのタグ名で並び替える HierarchySort を作成してみます。

BaseHierarchySort を継承したクラスを作成する

先ほど AlphaNumericSort のサンプルで見たように BaseHierarchySort クラスを継承した TagSort クラスを作成します。

using UnityEngine;
using UnityEditor;
public class TagSort : BaseHierarchySort
{
}

Compare をオーバーライドして比較を行う

次に、ゲームオブジェクトの タグで比較するように実装します。

using UnityEngine;
using UnityEditor;
public class TagSort : BaseHierarchySort
{
  public override int Compare (GameObject lhs, GameObject rhs)
  {
    if (lhs == rhs) return 0;
    if (lhs == null) return -1;
    if (rhs == null) return 1;
    return EditorUtility.NaturalCompare (lhs.tag, rhs.tag);
  }
}
EditorOnly と MainCamera のタグが設定されており、タグ名のアルファベット順に並んでいることが分かる

図18.2: EditorOnly と MainCamera のタグが設定されており、タグ名のアルファベット順に並んでいることが分かる

これ自体で Tag を使ってのソートができるようになりました。インスペクターを見てみると確かに並び替わっていることがわかりますが、GameObject がどの Tag を持っているのか Hierarchy ウィンドウを見た限りではわかりません。

Hierarchy のゲームオブジェクト名の横にタグ名を表示する

そこで EditorApplication.hierarchyWindowItemOnGUI を使用してタグ名を表示したいと思います。

using UnityEngine;
using UnityEditor;

[InitializeOnLoad]
public class TagSort : BaseHierarchySort
{
  static TagSort()
  {
    EditorApplication.hierarchyWindowItemOnGUI += (instanceID, selectionRect) =>
    {
      var go = EditorUtility.InstanceIDToObject(instanceID) as GameObject;
      selectionRect.x += selectionRect.width - 64;
      selectionRect.width = 64;
      style.fontSize = 8;
      EditorGUI.LabelField(selectionRect, go.tag, Style.miniBox);
    };
  }

  public override int Compare(GameObject lhs, GameObject rhs)
  {
    if (lhs == rhs)
      return 0;
    if (lhs == null)
      return -1;
    if (rhs == null)
      return 1;
    return EditorUtility.NaturalCompare(lhs.tag, rhs.tag);
  }

  private class Style
  {
    public static GUIStyle miniBox;

    static Style ()
    {
      miniBox = new GUIStyle ("box");
      miniBox.fontSize = 8;
    }
  }
}

表示を行うだけなら上記の書き方でいいと思うかもしれません。ですがこの書き方は「TagSort を選択していなくても Tag 名が表示されてしまう」という状況によっては「厄介だ」と思う方もいると思います。

18.3 現在何でソートされているかを知る

なので今回は「TagSort を選択している時だけタグ名を表示する」という手法を取りたいと思います。現在何でソートされているかを知るには、API として正式に提供されていないため少し面倒くさいことをしなければいけません。面倒くさいことをするためにまず、Hierarchy ウィンドウの Type を取得しなければいけません。その際には UnityEngine.Types.GetType を使用すると便利です。Types.GetType は Assembly から特定の Type を取得するための API となっています。

Type hierarcyType =
    Types.GetType("UnityEditor.SceneHierarchyWindow", "UnityEditor.dll");

次に、先ほど取得した hierarcyType を使用して Hierarchy ウィンドウを取得します。

EditorWindow hierarcyWindow = EditorWindow.GetWindow(hierarcyType, false);

複数対応する場合は Resources.FindObjectsOfTypeAll(Type);を使用してすべての Hierarchy ウィンドウを取得しますが複雑になってしまうので今回は単体ウィンドウのみ対応で実装を行います。Hierarchy ウィンドウのオブジェクトの中に、Serialize された m_CurrentSortMethod という文字列データが存在します。これが現在選択されているソート名となります。

m_CurrentSortMethod は Serialize されているデータなので SerializedObject#FindProperty で取得するようにしましょう。そのためにはまず Hierarchy ウィンドウの SerializedObject を取得しなければいけません。今回は SerializedObject を取得ではなく作成します。SerializedObject を使用する場面は普段は CustomEditor などで、すでに作成された SerializedObject を使ってアクセスしている方が多いと思います。なので、自分で作成するのは初めての方も多いのではないでしょうか。

SerializedObject hierarcyWindowObject = new SerializedObject(hierarcyWindow);

こうして現在のソート名を取得することが可能になりました。

var currentSortName =
    hierarcyWindowObject.FindProperty("m_CurrentSortMethod").stringValue;

ですがこれだけではソート名を取得するには不十分です。確かに現在のタグ名を取得しているように見えますが細かく言うと「SerializedObject 作成時のソート名を取得している」ことになっています。「最新の SerializedObject からソート名を取得するためにはSerializedObject#Update を呼び出してあげてください。こうすることによって常に最新の「現在のソート名」を取得することが可能です。

現在のソート名が TagSort 名以外であればタグ表示をやめる

ここまでくれば残りの実装は簡単です。

EditorApplication.hierarchyWindowItemOnGUI += HierarchyWindowItemOnGUI;

private void HierarchyWindowItemOnGUI(int instanceID, Rect selectionRect)
{
  hierarcyWindowObject.Update();
  var currentSortName =
      hierarcyWindowObject.FindProperty("m_CurrentSortMethod").stringValue;

  if (currentSortName != "TagSort")
  {
    EditorApplication.hierarchyWindowItemOnGUI -= HierarchyWindowItemOnGUI;
    hierarcyWindowObject = null;
    return;
  }

  //Tag 名表示
}

HierarchySort のアイコンを変更

TagSort 専用のアイコンに変更します。

図18.3:

用意したアイコンを「Editor Default Resources/Icons」フォルダーに格納します。

図18.4:

これにより、EditorGUIUtility.IconContent ("TagSort");でアイコンをロードすることが可能になりました。このアイコンを適用するために、content 変数をオーバーライドします。

public class TagSort : BaseHierarchySort
{
    private GUIContent m_content = EditorGUIUtility.IconContent ("TagSort");

    public override GUIContent content {
        get {
            return m_content;
        }
    }

    //略
}

実際のコードがこちらになります。

using UnityEngine;
using UnityEditor;

public class TagSort : BaseHierarchySort
{
  private SerializedObject hierarcyWindowObject;

  public override int Compare (GameObject lhs, GameObject rhs)
  {
    if (lhs == rhs)
      return 0;
    if (lhs == null)
      return -1;
    if (rhs == null)
      return 1;
    return EditorUtility.NaturalCompare (lhs.tag, rhs.tag);
  }
  private GUIContent m_content = EditorGUIUtility.IconContent ("TagSort");

  public override GUIContent content {
    get {

      if (hierarcyWindowObject == null) {
        var hierarcyType =
          Types.GetType ("UnityEditor.SceneHierarchyWindow", "UnityEditor.dll");
        var hierarcyWindows = Resources.FindObjectsOfTypeAll (hierarcyType);
        if (hierarcyWindows.Length != 0) {
          hierarcyWindowObject = new SerializedObject (hierarcyWindows[0]);
          EditorApplication.hierarchyWindowItemOnGUI += HierarchyWindowItemOnGUI;
        }
      }

      return m_content;
    }
  }

  private void HierarchyWindowItemOnGUI (int instanceID, Rect selectionRect)
  {
    hierarcyWindowObject.Update ();

    var currentSortName =
      hierarcyWindowObject.FindProperty ("m_CurrentSortMethod").stringValue;

    if (currentSortName != "TagSort") {
      EditorApplication.hierarchyWindowItemOnGUI -= HierarchyWindowItemOnGUI;
      hierarcyWindowObject = null;
      return;
    }

    var go = EditorUtility.InstanceIDToObject (instanceID) as GameObject;

    selectionRect.x += selectionRect.width - 64;
    selectionRect.width = 64;

    EditorGUI.LabelField (selectionRect, go.tag, Style.miniBox);
  }

  private class Style
  {
    public static GUIStyle miniBox;

    static Style ()
    {
      miniBox = new GUIStyle ("box");
      miniBox.fontSize = 8;
    }
  }
}
第17章 Handle(ハンドル) 第19章 GUI を自作する