はじめに
Unityで当たり判定の管理にboolの2次元配列(bool[,]
)を使いたかったが、調べても自分の使用したいものがなかったので作成しました。結果的に次のように変数宣言するだけで画像のようなエディターで値を編集できるので満足しています。
public BoolPlane BoolArray;
重要な部分以外は省略しているので、使用する際や全文を把握したい場合はGithubをご覧ください。
Unityのバージョンは2021.3.5fを使用しています。
目次
- シリアライズ可能なクラスを作成
- OnBeforeSerialize
- OnAfterDeserialize
- EditorWindowを作成
- 配列のサイズ変更
- 値の自動保存
- PropertyDrawerを作成
シリアライズ可能なクラスを作成
データが保存できなければ、表示できても意味がないので、シリアライズを実装します。
今回は2次元配列を1次元配列に変換することでシリアライズを実現しています。
手順
-
ISerializationCallbackReceiver
を継承したクラスを作成する -
OnBeforeSerialize
にシリアライズ処理を記述する -
OnAfterDeserialize
にデシリアライズの処理を記述する
[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を作成
値を編集するために次のようなウィンドウを作成します。
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上にボタンを配置して先程作成したエディターを呼び出します。
[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()
のブロックでは編集の対象となるオブジェクトを見つけるために、リフレクションを使用して複雑な処理を行っているので詳しく解説します。
始めに、targetObject
でBoolPlane
が使用されているオブジェクトを取得します。
var obj = property.serializedObject.targetObject;
例として次のようなスクリプトがあった場合、targetObject
はTest
になります。
class Test : MonoBehaviour
{
public BoolPlane planeObject;
}
次にTest
から編集対象となるBoolPlane
を探したいのでフィールドを全て取得して、以下の条件に一致するフィールドを発見します。
- 値がNullではない
- オブジェクトの型が
BoolPlane
である - オブジェクト名を小文字に変換した文字列が等しい
3番のように面倒なことをするには理由があります。label.text
はUnityのInspector上で表示される名称だからです。例えば上記のTest.csのplaneObject
はlabel.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