2018年8月1日

Unity ヒエラルキーを並べ替える


"Hierarchy"タブに並んだゲームオブジェクトをソートする方法をメモメモ…


※このページの内容の動作確認にはUnity5.6を使用しています。
エディタ拡張でヒエラルキー上のオブジェクトをソートするコードを書いてみたのでとりあえずメモしておきます。カタカナ多いですね。ちなみに、こちらで解説されているような一時的に表示順を変える方法ではなく、実際にTransformの値を変更してオブジェクトの並び順(取得順)を変更する方法なので注意。

使い方は、まず次のような内容のSortHierarchyというスクリプトファイルを作成しEditorフォルダに突っ込みます。するとUnityのメニューにあるGameObjectの項目下にSort > By Nameなどのボタンが表示されるので、あとはヒエラルキー上で並べ替えたいオブジェクトを複数選択した状態でそのメニューのボタンを押すだけです。Undoもできるので「ミスった!」と思ったら焦らずCtrl+Zで戻ってください。



using System;
using System.Linq;
using UnityEditor;
using UnityEngine;

public class SortHierarchy : MonoBehaviour {

 const string itemname_byname = "GameObject/Sort/By Name";
 const string itemname_byposx = "GameObject/Sort/By Position X";
 const string itemname_byposy = "GameObject/Sort/By Position Y";
 const string itemname_byposz = "GameObject/Sort/By Position Z";
 
 // アルファベット順に並べ替え
 [MenuItem(itemname_byname, false, 130)]
 static void SortByName()
 {
  Sort((a, b) => string.Compare(a.name, b.name));
 }

 // X座標の位置で並べ替え
 [MenuItem(itemname_byposx, false, 130)]
 static void SortByPosX()
 {
  Sort((a, b) => a.position.x.CompareTo(b.position.x));
 }

 // Y座標の位置で並べ替え
 [MenuItem(itemname_byposy, false, 130)]
 static void SortByPosY()
 {
  Sort((a, b) => a.position.y.CompareTo(b.position.y));
 }

 // Z座標の位置で並べ替え
 [MenuItem(itemname_byposz, false, 130)]
 static void SortByPosZ()
 {
  Sort((a, b) => a.position.z.CompareTo(b.position.z));
 }

 // 現在選択中のオブジェクトを指定した比較方法で並べ替える
 static void Sort(Comparison<Transform> compare)
 {
  var selected = Selection.transforms.GroupBy(s => s.parent);
  foreach (var group in selected)
  {
   var sorted = group.ToList();
   sorted.Sort(compare);

   var indices = sorted.Select(s => s.GetSiblingIndex()).OrderBy(s => s).ToList();
   
   for (int i = 0; i < sorted.Count; i++)
   {
    Undo.SetTransformParent(sorted[i], sorted[i].parent, "Sort");
    sorted[i].SetSiblingIndex(indices[i]);
   }
  }
 }
 
 // オブジェクトが選択されていなければメニューを無効化する
 [MenuItem(itemname_byname, true)]
 [MenuItem(itemname_byposx, true)]
 [MenuItem(itemname_byposy, true)]
 [MenuItem(itemname_byposz, true)]
 static bool ValidateSort()
 {
  return Selection.transforms.Any();
 }
}

メニューの表示位置や項目名、ソートの方法、その他気に入らない書き方等あれば自由に追加・変更しちゃってください。

てなわけで一応ポイントだけ説明しておきます。

[MenuItem(itemname_byname, false, 130)]

MenuItem属性を使えば指定した位置にメニューを追加することができます。1つ目の引数はメニュー項目の名前とパスです。3つ目の引数は項目の優先順位で、数が大きい項目ほど下のほうに表示されます。ちなみに優先順位を49以下にしておくとヒエラルキー上の右クリックメニューにも表示されるようになります。ショートカットコマンドも指定できるので使いやすいようにカスタマイズしてみてください。

[MenuItem(itemname_byname, true)]
[MenuItem(itemname_byposx, true)]
[MenuItem(itemname_byposy, true)]
[MenuItem(itemname_byposz, true)]
static bool ValidateSort()
{
 return Selection.transforms.Any();
}

MenuItem属性の2つ目の引数をtrueに設定するとそのメソッドは「メニュー項目ON/OFF切り替え用メソッド」として機能するようになります。returnで返した真偽値に応じて、第1引数で指定したメニューを有効化させたり無効化(グレーアウト)させたりできます。まあぶっちゃけ別になくてもいいのですが、メニュー操作がわかりやすくなるので一応設定しておきましょう。ここではSelection.transforms、つまりシーン内で現在選択中のオブジェクトがあるかどうかでON/OFFを切り替えています。

左はオブジェクトを選択している状態 右は何も選択していない状態


その他諸々MenuItemに関する基本的なことはUnityのチュートリアルでも解説されています。

var selected = Selection.transforms.GroupBy(s => s.parent);

実際の並び替えの処理では、まず同じ親を持つオブジェクトどうしをグループ分けして、そのまとまりごとにソートを行っています。別々の階層のオブジェクトまで一緒くたにして順番を決めるとなるとといろいろややこしいので…。それと、現在選択中のtransformを一括取得できるSelection.transformsですが、どうやら「親オブジェクトと直下の子オブジェクトを同時に選択した場合は子オブジェクトが選択されていないことになる」仕様のようです。説明に"Returns the top level selection"って書いてあるのでたぶんそういうことなんでしょう。並び替えが実行されない!というときは間違って親オブジェクトまで含めて選択していないか確認してみてください。

var sorted = group.ToList();
sorted.Sort(compare);

47、48行目。本当はLinqのOrderByとかを使ってもうちょっとカッコよく書きたかったのですが、ソート条件を外部から渡す方法がよくわからなかったので諦めました。というわけで普通にリスト化してSortを使うことに。これならComparison<T>で手軽に条件を指定できていいですね。シンプルなのが一番じゃ~!

var indices = sorted.Select(s => s.GetSiblingIndex()).OrderBy(s => s).ToList();
sorted[i].SetSiblingIndex(indices[i]);

ヒエラルキーの同じ階層の中でのオブジェクト順を取得/変更したい場合はGetSiblingIndexとSetSiblingIndexを使えばOKです。

Undo.SetTransformParent(sorted[i], sorted[i].parent, "Sort");

Undoを登録したい場合は通常Undo.RecordObjectを使っておけばいいのですが、今回はそれだとうまく変更が保存されません。調べてみたところUndo.SetTransformParentを使えばいいという情報を発見。試してみたらちゃんとUndoできるようになりました。別に親子関係を設定しているわけではないのですが、たぶんインデックス(並び順)の情報も一緒に保存されているんでしょう。もしかしたらUndo.RegisterFullObjectHierarchyUndoのほうがいいような気がしないでもないですが(名前的に)、説明を読むかぎりなんかすごく仰々しい感じだったのでやめておきました。


降順やタグ名での並び替えなどソートの種類を追加するのも比較的簡単だと思います。Sortメソッドにパラメータとして渡すだけなので…。Linqとかforeachとかforとかがごちゃまぜでまだまだ全体的にこなれていない感MAXのコードなのはもうちょっとなんとかしたいです。

0 件のコメント:

コメントを投稿