少々長くなってしまいすみませんが、一案として下記のようなものはどうでしょうかね?
とはいえ、このやり方だとFoodMenu
を使う側が必要に応じてfood
を適切な型にキャストしてやる必要があるでしょう。型安全性の観点からは1570pさんの方針の方が好ましいかもしれません。
lang
1 using System;
2 using System.Collections.Generic;
3 using System.Linq;
4 using System.Reflection;
5 using UnityEngine;
6 #if UNITY_EDITOR
7 using UnityEditor;
8 #endif
9
10 // スクリプトからの使用時は、一例としては下記のようになります
11 public class JOE : MonoBehaviour
12 {
13 [SerializeField] private FoodMenu breakfast;
14 [SerializeField] private FoodMenu lunch;
15 [SerializeField] private FoodMenu dinner;
16
17 private void Start()
18 {
19 PrintMeal(breakfast, "朝");
20 PrintMeal(lunch, "昼");
21 PrintMeal(dinner, "晩");
22 }
23
24 private static void PrintMeal(FoodMenu menu, string period)
25 {
26 var foodTypeString = "謎の料理";
27 if (menu.foodType == typeof(JapaneseFood))
28 {
29 foodTypeString = "日本料理";
30 }
31 else if (menu.foodType == typeof(ChineseFood))
32 {
33 foodTypeString = "中華料理";
34 }
35
36 var foodString = menu.food.ToString();
37 if (foodString == "NONE")
38 {
39 Debug.Log($"今日の{period}ご飯無し!!!");
40 }
41 else
42 {
43 Debug.Log($"今日の{period}ご飯は{foodTypeString}の{foodString}です");
44 }
45 }
46 }
47
48 [Serializable]
49 public class FoodMenu : ISerializationCallbackReceiver
50 {
51 // インスペクター上のポップアップに表示するのを許可する型をリストにしておき...
52 public static readonly List<Type> FoodTypes = new List<Type>
53 {
54 typeof(JapaneseFood),
55 typeof(ChineseFood)
56 };
57
58 // その先頭の型をデフォルトの型としました
59 private static readonly Type DefaultType = FoodTypes[0];
60
61 // また、デフォルトの食事は「NONE」としました
62 // どの国の料理にも共通して含まれる値であることを前提としています
63 private static readonly string DefaultFoodString = "NONE";
64
65 // foodTypeやfoodを直接シリアライズする代わりに...
66 [NonSerialized] public Type foodType;
67 [NonSerialized] public Enum food;
68
69 // foodTypeの型名とfoodの列挙名を文字列としてシリアライズすることにしました
70 [SerializeField][HideInInspector] private string serializedFoodType;
71 [SerializeField][HideInInspector] private string serializedFood;
72
73 public void OnBeforeSerialize()
74 {
75 // foodTypeがnullならデフォルトの型でシリアライズし...
76 foodType ??= DefaultType;
77 if (!foodType.IsEnum)
78 {
79 Debug.LogError($"{foodType} is not an enum type.");
80 return;
81 }
82 serializedFoodType = foodType.AssemblyQualifiedName;
83
84 // foodがnullならデフォルトの食事でシリアライズしました
85 food ??= (Enum)Enum.Parse(foodType, DefaultFoodString);
86 serializedFood = food.ToString();
87 }
88
89 public void OnAfterDeserialize()
90 {
91 // デシリアライズ時には文字列からTypeやEnumを復元します
92 // 失敗すればデフォルト値としました
93 var deserializedFoodType = (string.IsNullOrWhiteSpace(serializedFoodType)
94 ? DefaultType
95 : Type.GetType(serializedFoodType, false)) ?? DefaultType;
96 if (!deserializedFoodType.IsEnum)
97 {
98 Debug.LogError($"{deserializedFoodType} is not an enum type.");
99 deserializedFoodType = DefaultType;
100 }
101
102 Enum deserializedFood;
103 try
104 {
105 deserializedFood = (Enum)Enum.Parse(
106 deserializedFoodType,
107 string.IsNullOrWhiteSpace(serializedFood)
108 ? DefaultFoodString
109 : serializedFood);
110 }
111 catch (ArgumentException)
112 {
113 // 型の変換時に失敗するのは異常事態ですのでエラーメッセージを出しましたが、
114 // ある国の料理名が別の国の料理名に含まれないということはありふれているように思い
115 // Debug.LogErrorによるメッセージは出さずにデフォルト値で置き換えることにしました
116 deserializedFood = (Enum)Enum.Parse(foodType, DefaultFoodString);
117 }
118
119 foodType = deserializedFoodType;
120 food = deserializedFood;
121 }
122 }
123
124 // 主食
125 public enum JapaneseFood
126 {
127 UDON,
128 SOBA,
129 NONE
130 }
131
132 public enum ChineseFood
133 {
134 RAMEN,
135 GYOZA,
136 NONE
137 }
138
139 // インスペクター上でポップアップを表示させるため、FoodMenu用のPropertyDrawerも作成しました
140 #if UNITY_EDITOR
141 [CustomPropertyDrawer(typeof(FoodMenu))]
142 public class FoodMenuDrawer : PropertyDrawer
143 {
144 // serializedFoodTypeやserializedFoodにアクセスするためのフィールド情報を取得しておきます
145 private static readonly FieldInfo SerializedFoodTypeField = typeof(FoodMenu).GetField(
146 "serializedFoodType",
147 BindingFlags.DeclaredOnly | BindingFlags.Instance | BindingFlags.NonPublic);
148 private static readonly FieldInfo SerializedFoodField = typeof(FoodMenu).GetField(
149 "serializedFood",
150 BindingFlags.DeclaredOnly | BindingFlags.Instance | BindingFlags.NonPublic);
151
152 // FoodMenuを一つ作っておき、シリアライズされた文字列と
153 // TypeやEnumの相互変換を担当させることにしました
154 private readonly FoodMenu menu = new FoodMenu();
155
156 // 後述のfoodType用ポップアップの項目名一覧です
157 private string[] cachedFoodTypeStrings;
158
159 // 後述の処理効率化のための前回取得文字列を保持するフィールドです
160 private string cachedSerializedFoodType;
161 private string cachedSerializedFood;
162
163 // 後述のように項目を折りたたみ式で表示するようにしました
164 // そこで項目が折り畳まれているかどうかも考慮し、必要な表示領域の高さを求めるようにします
165 public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
166 {
167 const int itemCount = 2;
168 var lineCount = property.isExpanded ? itemCount + 1 : 1;
169 return (lineCount * EditorGUIUtility.singleLineHeight) + ((lineCount - 1) * EditorGUIUtility.standardVerticalSpacing);
170 }
171
172 public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
173 {
174 EditorGUI.BeginProperty(position, label, property);
175
176 // 折りたたみ式で表示することにしました
177 position.height = EditorGUIUtility.singleLineHeight;
178 property.isExpanded = EditorGUI.Foldout(position, property.isExpanded, label);
179 if (property.isExpanded)
180 {
181 // 折りたたみが展開されていれば各項目の描画に進みます
182 // まずpropertyからシリアライズされた文字列を取得し...
183 var serializedFoodTypeProperty = property.FindPropertyRelative("serializedFoodType");
184 var serializedFoodProperty = property.FindPropertyRelative("serializedFood");
185 var serializedFoodType = serializedFoodTypeProperty.stringValue;
186 var serializedFood = serializedFoodProperty.stringValue;
187
188 // menuに文字列をType、Enumに変換させます
189 // このとき、多少なりとも効率化しようと思い、前回取得時の文字列と
190 // 異なっている時のみ変換を行うことにしました
191 if ((serializedFoodType != cachedSerializedFoodType) || (serializedFood != cachedSerializedFood))
192 {
193 SerializedFoodTypeField.SetValue(menu, serializedFoodTypeProperty.stringValue);
194 SerializedFoodField.SetValue(menu, serializedFoodProperty.stringValue);
195 menu.OnAfterDeserialize();
196 cachedSerializedFoodType = serializedFoodType;
197 cachedSerializedFood = serializedFood;
198 }
199
200 // 変換結果からfoodTypeのインデックス(foodTypeがFoodMenu.FoodTypesリスト中の何番目か)と
201 // foodを取得し...
202 var currentFoodTypeIndex = Mathf.Max(FoodMenu.FoodTypes.IndexOf(menu.foodType), 0);
203 var currentFood = menu.food;
204
205 EditorGUI.indentLevel++;
206
207 // 1項目分の垂直移動量を求め、次の項目位置までずらし...
208 var stride = EditorGUIUtility.singleLineHeight + EditorGUIUtility.standardVerticalSpacing;
209 position.y += stride;
210
211 // ラベル部分とポップアップ部分の領域を計算し...
212 const float minValueWidth = 16.0f;
213 var labelPosition = position;
214 var valuePosition = position;
215 labelPosition.width = EditorGUIUtility.labelWidth;
216 valuePosition.width = position.width - labelPosition.width;
217 if (valuePosition.width <= minValueWidth)
218 {
219 labelPosition.width = valuePosition.width = position.width * 0.5f;
220 }
221 valuePosition.x += labelPosition.width;
222
223 // foodType用ラベルとポップアップを描画、選択された項目インデックスを取得し...
224 EditorGUI.LabelField(labelPosition, nameof(menu.foodType));
225 cachedFoodTypeStrings ??= FoodMenu.FoodTypes.Select(t => t.Name).ToArray();
226 var newFoodTypeIndex = EditorGUI.Popup(valuePosition, currentFoodTypeIndex, cachedFoodTypeStrings);
227
228 // 次の行へ進み...
229 labelPosition.y += stride;
230 valuePosition.y += stride;
231
232 // food用ラベルとポップアップを描画、選択されたEnumを取得し...
233 EditorGUI.LabelField(labelPosition, nameof(menu.food));
234 var newFood = EditorGUI.EnumPopup(valuePosition, currentFood);
235
236 // 最後に、インスペクター操作によって値が変化していればプロパティを更新します
237 if ((newFoodTypeIndex != currentFoodTypeIndex) || !Equals(newFood, currentFood))
238 {
239 menu.foodType = FoodMenu.FoodTypes[newFoodTypeIndex];
240 menu.food = newFood;
241 menu.OnBeforeSerialize();
242 serializedFoodTypeProperty.stringValue = SerializedFoodTypeField.GetValue(menu) as string;
243 serializedFoodProperty.stringValue = SerializedFoodField.GetValue(menu) as string;
244 }
245
246 EditorGUI.indentLevel--;
247 }
248
249 EditorGUI.EndProperty();
250 }
251 }
252 #endif