📗

【Unity】Inspectorで設定できるDictionaryを作りたい

2024/02/11に公開

はじめに

初めての方も、そうでない方もこんにちは!
現役ゲームプログラマーのたむぼーです。
自己紹介を載せているので、気になる方は見ていただければ嬉しいです!

今回は
 UnityのInspectorで設定できるDictionaryの作り方
を紹介します

https://zenn.dev/tmb/articles/1072f8ea010299

実際の画面

こちらが、実際の画面です。

Keyにはenum、Valueにはstringを設定しています

よくあるInspectorで設定できるDictionary

先に他の記事などでも取り上げられている
Inspectorで設定できるDictionaryの設計を紹介します。

SerializableDictionary

SerializableDictionary.cs
using System;
using UnityEngine;
using System.Collections.Generic;

/// <summary>
/// SerializableDictionary
/// </summary>
[Serializable]
public class SerializableDictionary<TKey, TValue, Type> where Type : Pair<TKey, TValue>
{
	[SerializeField]
	private List<Type> _list = null;

	private Dictionary<TKey, TValue> _dictionary = null;
	public Dictionary<TKey, TValue> Dictionary
	{
		get
		{
			_dictionary ??= ConvertToDictionary();
			return _dictionary;
		}
	}

	/// <summary>
	/// ConvertToDictionary
	/// </summary>
	private Dictionary<TKey, TValue> ConvertToDictionary()
	{
		Dictionary<TKey, TValue> dictionary = new Dictionary<TKey, TValue> ();
		foreach(Pair<TKey, TValue> pair in _list)
		{
			dictionary.Add(pair.key, pair.value);
		}
		return dictionary;
	}
}

/// <summary>
/// Pair
/// </summary>
[Serializable]
public class Pair<TKey, TValue>
{
	public TKey key;
	public TValue value;

	public Pair(TKey key, TValue value)
	{
		this.key = key;
		this.value = value;
	}
}

使用例(ExampleSerializableDictionary1)

ExampleSerializableDictionary1.cs
using System;
using System.Collections.Generic;
using UnityEngine;

public class ExampleSerializableDictionary1 : MonoBehaviour
{
	public enum Keys
	{
		Key1,
		Key2,
		Key3,
		Key4,
		Key5,
	}

	[SerializeField]
	private SampleDictionary _sampleDictionary;

	private void Start()
	{
		Debug.Log("------------------------------");
		Debug.Log("1回目: インスペクターで設定した状態");
		Debug.Log("----------");

		foreach (KeyValuePair<Keys, string> pair in _sampleDictionary.Dictionary)
		{
			Debug.Log(string.Format("{0} -> {1}", pair.Key, pair.Value));
		}

		Debug.Log("------------------------------");
		Debug.Log("2回目: Key5を追加");
		Debug.Log("----------");

		_sampleDictionary.Dictionary.Add(Keys.Key5, "Key5は今追加しました。");
		foreach (KeyValuePair<Keys, string> pair in _sampleDictionary.Dictionary)
		{
			Debug.Log(string.Format("{0} -> {1}", pair.Key, pair.Value));
		}

		Debug.Log("------------------------------");
		Debug.Log("3回目: Key3を削除");
		Debug.Log("----------");

		_sampleDictionary.Dictionary.Remove(Keys.Key3);
		foreach (KeyValuePair<Keys, string> pair in _sampleDictionary.Dictionary)
		{
			Debug.Log(string.Format("{0} -> {1}", pair.Key, pair.Value));
		}

		Debug.Log("------------------------------");
	}
}
/// <summary>
/// SampleDictionary
/// </summary>
[Serializable]
public class SampleDictionary : SerializableDictionary<ExampleSerializableDictionary1.Keys, string, SamplePair>
{
}

/// <summary>
/// SamplePair
/// </summary>
[Serializable]
public class SamplePair : Pair<ExampleSerializableDictionary1.Keys, string>
{
	public SamplePair(ExampleSerializableDictionary1.Keys key, string value) : base(key, value)
	{
	}
}
実行結果
------------------------------
1回目: インスペクターで設定した状態
----------
Key1 -> テスト1
Key2 -> テスト2
Key3 -> テスト3
Key4 -> テスト4
------------------------------
2回目: Key5を追加
----------
Key1 -> テスト1
Key2 -> テスト2
Key3 -> テスト3
Key4 -> テスト4
Key5 -> Key5は今追加しました。
------------------------------
3回目: Key3を削除
----------
Key1 -> テスト1
Key2 -> テスト2
Key4 -> テスト4
Key5 -> Key5は今追加しました。
------------------------------

使用方法

  1. SerializableDictionary.csで定義したPairを継承し、任意のKeyとValueの型を指定したクラス(例:SamplePair)を作り、コンストラクタを作成してbaseに渡す

  2. SerializableDictionary.csで定義したSerializableDictionaryを継承して、任意のKeyとValueの型と1で作成したクラス(例:SamplePair)を指定したクラス(例:SampleDictionary)を作る

  3. 2で作ったクラス(例:SampleDictionary)を利用したい箇所で[SerializeField]属性を付けた変数を定義

これで、Inspectorで設定できるDictionaryが作れます。

しかし、面倒・・・

というのも、Inspectorで設定できるDictionaryの数分
SamplePairSampleDictionaryのような継承クラスを作る必要があります。

改良版 - Inspectorで設定できるDictionary

なので、今回は面倒な手間を省いた
Inspectorで設定できるDictionaryを作ります

SerializableDictionary

SerializableDictionary.cs
using System;
using System.Collections.Generic;
using UnityEngine;

[Serializable]
public class SerializableDictionary<TKey, TValue> :
	Dictionary<TKey, TValue>,
	ISerializationCallbackReceiver
{
	[Serializable]
	public class Pair
	{
		public TKey key = default;
		public TValue value = default;

		/// <summary>
		/// Pair
		/// </summary>
		public Pair(TKey key, TValue value)
		{
			this.key = key;
			this.value = value;
		}
	}

	[SerializeField]
	private List<Pair> _list = null;

	/// <summary>
	/// OnAfterDeserialize
	/// </summary>
	void ISerializationCallbackReceiver.OnAfterDeserialize()
	{
		Clear();
		foreach (Pair pair in _list)
		{
			if (ContainsKey(pair.key))
			{
				continue;
			}
			Add(pair.key, pair.value);
		}
	}

	/// <summary>
	/// OnBeforeSerialize
	/// </summary>
	void ISerializationCallbackReceiver.OnBeforeSerialize()
	{
		// 処理なし
	}
}

使用例(ExampleSerializableDictionary2)

基本的には、通常のDictionary同じように使えると思います

ExampleSerializableDictionary2.cs
using System.Collections.Generic;
using UnityEngine;

public class ExampleSerializableDictionary2 : MonoBehaviour
{
	public enum Keys
	{
		Key1,
		Key2,
		Key3,
		Key4,
		Key5,
	}

	[SerializeField]
	private SerializableDictionary<Keys, string> _dictionary = null;

	/// <summary>
	/// Start
	/// </summary>
	private void Start()
	{
		Debug.Log("------------------------------");
		Debug.Log("1回目: インスペクターで設定した状態");
		Debug.Log("----------");

		foreach (KeyValuePair<Keys, string> pair in _dictionary)
		{
			Debug.Log(string.Format("{0} -> {1}", pair.Key, pair.Value));
		}

		Debug.Log("------------------------------");
		Debug.Log("2回目: Key5を追加");
		Debug.Log("----------");

		_dictionary.Add(Keys.Key5, "Key5は今追加しました。");
		foreach (KeyValuePair<Keys, string> pair in _dictionary)
		{
			Debug.Log(string.Format("{0} -> {1}", pair.Key, pair.Value));
		}

		Debug.Log("------------------------------");
		Debug.Log("3回目: Key3を削除");
		Debug.Log("----------");

		_dictionary.Remove(Keys.Key3);
		foreach (KeyValuePair<Keys, string> pair in _dictionary)
		{
			Debug.Log(string.Format("{0} -> {1}", pair.Key, pair.Value));
		}

		Debug.Log("------------------------------");
	}
}
実行結果
------------------------------
1回目: インスペクターで設定した状態
----------
Key1 -> テスト1
Key2 -> テスト2
Key3 -> テスト3
Key4 -> テスト4
------------------------------
2回目: Key5を追加
----------
Key1 -> テスト1
Key2 -> テスト2
Key3 -> テスト3
Key4 -> テスト4
Key5 -> Key5は今追加しました。
------------------------------
3回目: Key3を削除
----------
Key1 -> テスト1
Key2 -> テスト2
Key4 -> テスト4
Key5 -> Key5は今追加しました。
------------------------------

使用方法

  1. 利用したい箇所で[SerializeField]属性を付けたSerializableDictionary変数を定義する、このときに合わせて任意のKeyとValueの型を指定する

以上

どうでしょうか?
少しは作りやすくなったのではないでしょうか?

解説

Listの特性

ListInspectorで設定できる特性を利用しています

例えば、このようなDataというクラスがあったとします。
そのクラスに[Serializable]属性を付け、Listに[SerializeField]属性を付けることで
Inspector上で操作が可能になります。

ちなみに、Dataクラスの変数はpublicである必要があります

ExampleList1.cs
using System;
using System.Collections.Generic;
using UnityEngine;

public class ExampleList1 : MonoBehaviour
{
	[Serializable]
	public class Data
	{
		public int key;
		public int value;
	}

	[SerializeField]
	private List<Data> _list = null;
}

Listの情報をDictionaryに変換する

ISerializationCallbackReceiver.OnAfterDeserialize(デシリアライズ後に呼ばれる関数)を利用して、Inspectorで設定された_listの情報を、そのままDictionaryAddで追加しています。

Dictionaryを継承しているので、そのままDictionaryの関数を使用することができます。

SerializableDictionary.cs
/// <summary>
/// OnAfterDeserialize
/// </summary>
void ISerializationCallbackReceiver.OnAfterDeserialize()
{
    Clear(); // DictionaryのClear;
    foreach (Pair pair in _list)
    {
        if (ContainsKey(pair.key))
    	{
            continue;
    	}
    	Add(pair.key, pair.value); // DictionaryのAdd();
    }
}

さいごに

いかがでしょうか?
今回はSampleDictionarySamplePairような関数を作らずに
Inspectorで設定できるDictionaryの作り方を紹介しました。
Dictionaryクラスを継承することで、うまい事できたような気がしてます。

もし比較検証してみて、今回紹介した内容が実用的でないなどあれば
(こそっと)教えていただけると嬉しいです!

Discussion