2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

【Unity】boolの2次元配列をシリアライズしてInspectorに表示する

Last updated at Posted at 2023-11-26

はじめに

Unityで当たり判定の管理にboolの2次元配列(bool[,])を使いたかったが、調べても自分の使用したいものがなかったので作成しました。結果的に次のように変数宣言するだけで画像のようなエディターで値を編集できるので満足しています。

public BoolPlane BoolArray;

重要な部分以外は省略しているので、使用する際や全文を把握したい場合はGithubをご覧ください。

Unityのバージョンは2021.3.5fを使用しています。

目次

  • シリアライズ可能なクラスを作成
    • OnBeforeSerialize
    • OnAfterDeserialize
  • EditorWindowを作成
    • 配列のサイズ変更
    • 値の自動保存
  • PropertyDrawerを作成

シリアライズ可能なクラスを作成

データが保存できなければ、表示できても意味がないので、シリアライズを実装します。
今回は2次元配列を1次元配列に変換することでシリアライズを実現しています。

手順

  1. ISerializationCallbackReceiverを継承したクラスを作成する
  2. OnBeforeSerializeにシリアライズ処理を記述する
  3. OnAfterDeserializeにデシリアライズの処理を記述する
BoolPlane.cs

[Serializable]
public class BoolPlane : ISerializationCallbackReceiver
{
    public bool[,] Value;
    private bool[] Serialize;

    private int m_width, m_height;

    public void OnAfterDeserialize()
    {
        Value = new bool[m_width, m_height];

        for (int x = 0; x < m_width; x++)
            for (int y = 0; y < m_height; y++)
            {
                Value[x, y] = Serialize[x + m_width * y];
            }
    }

    public void OnBeforeSerialize()
    {
        var w = Value.GetLength(0);
        var h = Value.GetLength(1);
        m_width = w;
        m_height = h;

        Serialize = new bool[w * h];

        for (int x = 0; x < w; x++)
            for (int y = 0; y < h; y++)
            {
                Serialize[x + w * y] = Value[x, y];
            }
    }
}

OnBeforeSerialize

Unityのスクリプトリファレンスによると、Unityがオブジェクトをシリアライズする前に呼び出されるようで、ここにシリアライズ処理を記述しなければいけません。上記のスクリプトではValueがシリアライズされる対象Serializeがシリアライズされた値になります。
下記の処理でValueの値をSerializeにコピーしています。

//前略
Serialize[x + w * y] = Value[x, y];

OnAfterDeserialize

OnBeforeSerializeとは逆でデシリアライズ処理を記述します。下記の処理でSerializeの値をValueにコピーしています。

//前略
Value[x, y] = Serialize[x + m_width * y];

EditorWindowを作成

値を編集するために次のようなウィンドウを作成します。

BoolPlaneWindow.cs
 public class BoolPlaneWindow : EditorWindow
 {
     //値の編集・更新
     SerializedObject serialized;
     Object obj;
     BoolPlane box;

     //編集対象
     bool[,] value;

     //EditorGUI用変数
     Vector2 scroll;
     Vector2Int planeSize;

     public static void ShowWindow(FieldInfo field, Object obj, SerializedObject serialized)
     {
         var window = GetWindow<BoolPlaneWindow>("Bool Array");

         //値の設定
         window.serialized = serialized;
         window.obj = obj;
         var box = field.GetValue(obj) as BoolPlane;
         window.box = box;
         window.value = box.Value;
         window.planeSize = new Vector2Int(box.Width, box.Height);
         window.Show();
     }

     private void OnGUI()
     {
         //配列のサイズを設定する
         EditorGUILayout.LabelField("Set array size:");
         EditorGUILayout.BeginHorizontal();
         planeSize = EditorGUILayout.Vector2IntField(GUIContent.none, planeSize);
         if (GUILayout.Button("Set"))
         {
             //配列のサイズ変更
             ResizeArray();

             //変更した配列をBoolPlaneに適用
             serialized.Update();
             box.Value = value;
             serialized.ApplyModifiedPropertiesWithoutUndo();

             //保存処理
             EditorUtility.SetDirty(obj);
             AssetDatabase.SaveAssets();
         }
         EditorGUILayout.EndHorizontal();
         //値を表示するエリア
         scroll = EditorGUILayout.BeginScrollView(scroll);

         for (int y = 0; y < value.GetLength(1); y++)
         {
             EditorGUILayout.BeginHorizontal();
             for (int x = 0; x < value.GetLength(0); x++)
             {
                 value[x, y] = EditorGUILayout.Toggle(value[x, y], GUILayout.Width(15));
             }
             EditorGUILayout.EndHorizontal();
         }
         EditorGUILayout.EndScrollView();
     }

     /// <summary>
     /// 配列をリサイズする
     /// </summary>
     private void ResizeArray()
     {
         var newValue = new bool[planeSize.x, planeSize.y];
         for (int x = 0; x < planeSize.x && x < value.GetLength(0); x++)
             for (int y = 0; y < planeSize.y && y < value.GetLength(1); y++)
             {
                 newValue[x, y] = value[x, y];
             }
         value = newValue;
     }

     private void OnDestroy()
     {
         //ウィンドウを閉じるときに値を保存する
         EditorUtility.SetDirty(obj);
         AssetDatabase.SaveAssets();
     }
 }

通常のエディター拡張とあまり変わらないので、重要な点だけ解説します。

配列のサイズ変更

配列のサイズを変更した際に、新しい配列を生成するので下記のように保存処理をする必要があります。

if (GUILayout.Button("Set"))
       {
           //配列のサイズ変更
           ResizeArray();

           //変更した配列をBoolPlaneに適用
           serialized.Update();
           box.Value = value;
           serialized.ApplyModifiedPropertiesWithoutUndo();

           //保存処理
           EditorUtility.SetDirty(obj);
           AssetDatabase.SaveAssets();
       }

値の自動保存

OnDestroyに保存処理を書くことで確実に値を保存することができます。

   private void OnDestroy()
   {
       //ウィンドウを閉じるときに値を保存する
       EditorUtility.SetDirty(obj);
       AssetDatabase.SaveAssets();
   }

PropertyDrawerを作成

PropertyDrawerを使うことでInspector上にBoolPlaneを表示出来るようになります。
Inspector上にボタンを配置して先程作成したエディターを呼び出します。

BoolPlaneDrawer.cs
 [CustomPropertyDrawer(typeof(BoolPlane))]
 public class BoolPlaneDrawer : PropertyDrawer
 {
     public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
     {
         EditorGUI.BeginProperty(position, label, property);
         // 子のフィールドをインデントしない 
         var indent = EditorGUI.indentLevel;
         EditorGUI.indentLevel = 0;
         // 矩形を計算
         var labelRect = new Rect(position.x, position.y, position.width * 0.4f, position.height);
         var sizeRect = new Rect(position.x + position.width * 0.4f, position.y, position.width * 0.4f, position.height);
         var buttonRect = new Rect(position.x + position.width * 0.8f + 10, position.y, position.width * 0.2f - 10, position.height);

         var sizeVec = new Vector2Int(property.FindPropertyRelative("m_width").intValue,
                                      property.FindPropertyRelative("m_height").intValue);

         //ラベル表示
         EditorGUI.PrefixLabel(labelRect, GUIUtility.GetControlID(FocusType.Passive), label);

         //サイズ表示
         EditorGUI.BeginDisabledGroup(true);
         EditorGUI.Vector2IntField(sizeRect, GUIContent.none, sizeVec);
         EditorGUI.EndDisabledGroup();

         //編集ウィンドウを表示する
         if (GUI.Button(buttonRect, "Edit"))
         {
             var obj = property.serializedObject.targetObject;
             var fields = obj.GetType().GetFields();

             //BoolPlaneを見つける
             foreach (var field in fields)
             {
                 var val = field.GetValue(obj);

                 if (val != null &&
                     val.GetType() == typeof(BoolPlane) &&
                     field.Name.ToLower() == label.text.Replace(" ", "").ToLower())
                 {
                     BoolPlaneWindow.ShowWindow(field, obj, property.serializedObject, label.text);
                 }
             }


         }
         // インデントを元通りに戻す
         EditorGUI.indentLevel = indent;

         EditorGUI.EndProperty();
     }
}

基本的にはUnity公式ドキュメントのPropertyDrawerのチュートリアルのコピーですが、GUI.Button()のブロックでは編集の対象となるオブジェクトを見つけるために、リフレクションを使用して複雑な処理を行っているので詳しく解説します。

始めに、targetObjectBoolPlaneが使用されているオブジェクトを取得します。

var obj = property.serializedObject.targetObject; 

例として次のようなスクリプトがあった場合、targetObjectTestになります。

Test.cs
class Test : MonoBehaviour
{
    public BoolPlane planeObject;
}

次にTestから編集対象となるBoolPlaneを探したいのでフィールドを全て取得して、以下の条件に一致するフィールドを発見します。

  1. 値がNullではない
  2. オブジェクトの型がBoolPlaneである
  3. オブジェクト名を小文字に変換した文字列が等しい

3番のように面倒なことをするには理由があります。label.textはUnityのInspector上で表示される名称だからです。例えば上記のTest.csのplaneObjectlabel.textだと"Plane Object"になるため、空白を削除したうえで小文字に揃えないと"planeObject"と"Plane Object"は等しくならないからです。
ただ、一つ注意点があり、"PLANEOBJECT"と"planeobject"といった同名の大文字小文字違いを判断できない点です。(通常は命名規則に違反するので、する人はいないと思います...)

//BoolPlaneを見つける
foreach (var field in fields)
{
     var val = field.GetValue(obj);

     if (val != null &&
         val.GetType() == typeof(BoolPlane) &&
         field.Name.ToLower() == label.text.Replace(" ", "").ToLower())
     {
         BoolPlaneWindow.ShowWindow(field, obj, property.serializedObject);
     }
}

最後に対象のBoolPlaneを発見できたので、BoolPlaneWindow.ShowWindow()を呼び出して、編集画面を表示して終了になります。

おわりに

Githubのほうに、bool以外のいくつかの型を用意してあるので興味あれば、ぜひ使用してください。
質問や感想等あれば、ぜひコメントお願いします。

参考サイト

https://docs.unity3d.com/ja/2021.2/ScriptReference/ISerializationCallbackReceiver.html
https://docs.unity3d.com/ja/2021.2/Manual/script-Serialization-Custom.html
https://kazupon.org/unity-no-edit-param-view-inspector/
https://docs.unity3d.com/ja/2021.3/Manual/editor-PropertyDrawers.html
https://docs.unity3d.com/ja/2021.1/ScriptReference/EditorGUILayout.html
https://docs.unity3d.com/ja/current/ScriptReference/GUI.html

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?