質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

新規登録して質問してみよう
ただいま回答率
85.48%
C#

C#はマルチパラダイムプログラミング言語の1つで、命令形・宣言型・関数型・ジェネリック型・コンポーネント指向・オブジェクティブ指向のプログラミング開発すべてに対応しています。

Unity

Unityは、Unity Technologiesが開発・販売している、IDEを内蔵するゲームエンジンです。主にC#を用いたプログラミングでコンテンツの開発が可能です。

Q&A

解決済

2回答

8261閲覧

Unityエディタ拡張で自作クラスのSerializedObject取得

kaitorisenkou

総合スコア28

C#

C#はマルチパラダイムプログラミング言語の1つで、命令形・宣言型・関数型・ジェネリック型・コンポーネント指向・オブジェクティブ指向のプログラミング開発すべてに対応しています。

Unity

Unityは、Unity Technologiesが開発・販売している、IDEを内蔵するゲームエンジンです。主にC#を用いたプログラミングでコンテンツの開発が可能です。

0グッド

0クリップ

投稿2020/05/30 03:05

編集2020/06/02 16:14

Unityについての質問です。

自作クラスを普通に[SeriarizeField]にするだけでは、インスペクタにおいて自作クラスの子スラスを格納・編集することができないので、
TextAssetから自作クラス又はその子クラスを取得し、その中のシリアライズ項目をインスペクタ表示・編集を行うというエディタ拡張を作ろうとしています。
そこで自作クラスのインスタンスからSeriarizedObjectをnewしようとしましたが、
ArgumentException: Object at index 0 is null
の例外が出てしまい、インスペクタ表示ができません。

ソースコード

自作クラスの例

C#

1using System.Collections; 2using System.Collections.Generic; 3using UnityEngine; 4 5[System.Serializable] 6public class RootClass{ 7 [SerializeField] protected string exampleValue = "Over."; 8 public string ExampleValue => exampleValue; 9 public virtual void Act() { 10 Debug.Log("This is RootClass." + exampleValue); 11 } 12} 13

当該スクリプト

C#

1using System; 2using UnityEngine; 3#if UNITY_EDITOR 4using UnityEditor; 5#endif 6 7[System.Serializable] 8[CreateAssetMenu(fileName = "parent", menuName = "CreateParent")] 9public class DataClass : ScriptableObject { 10 [SerializeField, HideInInspector] string className = null; 11 public string ClassName => className; 12 [SerializeField, HideInInspector] RootClass rootClassData = null; 13 public RootClass RootClassData => rootClassData; 14 15 virtual public void Act(GameObject target) { 16 rootClassData.Act(); 17 } 18 19 public void SetClassName(string className) { 20 this.className = className; 21 rootClassData = null; 22 } 23 public void SetParentClass(RootClass parentClass) { 24 this.rootClassData = parentClass; 25 } 26} 27#if UNITY_EDITOR 28[CustomEditor(typeof(DataClass))] 29public class DataClassEditor : Editor { 30 /* 31 private void OnEnable() { 32 } 33 */ 34 DataClass targetDC => ((DataClass)target); 35 public override void OnInspectorGUI() { 36 serializedObject.Update(); 37 TextAsset behaviourScript; 38 39 string className; 40 behaviourScript = EditorGUILayout.ObjectField("BehaviourScript", null, typeof(TextAsset), false) as TextAsset; 41 if (behaviourScript != null) { 42 className = behaviourScript.name; 43 } else { 44 className = targetDC.ClassName; 45 } 46 47 if (className == null) { 48 EditorGUILayout.HelpBox("ClassName is null.", MessageType.Error); 49 } else { 50 if (className == "") { 51 EditorGUILayout.HelpBox("ClassName is blank.", MessageType.Error); 52 } else { 53 Type type = Type.GetType(className); 54 if (behaviourScript != null) 55 targetDC.SetClassName(behaviourScript.name); 56 EditorGUILayout.HelpBox("Attached: " + className, MessageType.Info); 57 58 var instance = Activator.CreateInstance(type); 59 60 //↓ここでArgumentException 61 var serObj = new SerializedObject(instance as UnityEngine.Object); 62 63 SerializedProperty prop = serObj.GetIterator(); 64 65 while (prop.Next(true)) { 66 EditorGUILayout.PropertyField(prop); 67 } 68 } 69 70 } 71 } 72} 73#endif

自作クラスの親クラスをUnityEngine.Object型にすることも試しましたが、同様のエラーが発生しました。
###やりたいことのイメージ
やりたいこと

気になる質問をクリップする

クリップした質問は、後からいつでもMYページで確認できます。

またクリップした質問に回答があった際、通知やメールを受け取ることができます。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

MMashiro

2020/06/01 17:21

> シリアライズ項目に応じたインスペクタ表示を行うというエディタ拡張を作ろうとしています。 このシリアライズ項目は編集し保存ができる必要はありますか? (デフォルト値の表示のみでOKなのか編集してScriptableObjectの中に変更後の値を保持する必要があるのか等です)
kaitorisenkou

2020/06/02 00:26

はい。今は編集保存できるようにしていませんが、いずれできるようにするつもりです。
guest

回答2

0

ベストアンサー

MMashiroさんがおっしゃるように、アセットの構造をちょっと変更してやった方が楽かもしれません。
スクリプトのシリアル化 - Unity マニュアルの「ポリモーフィズムはサポートされません」の節では、対処案として別のオブジェクトを参照する形にするようなことが書かれていました。RootClass(あるいはそれの派生クラス)もアセット化して、メインのアセットからそれを参照するというのはどうでしょうか?

まずRootClassScriptableObjectにしてしまい...

C#

1using UnityEngine; 2 3public class RootClass : ScriptableObject 4{ 5 [SerializeField] protected string exampleValue = "Over."; 6 public string ExampleValue => this.exampleValue; 7 8 public virtual void Act() 9 { 10 Debug.Log("This is RootClass." + this.exampleValue); 11 } 12}

それによってRootClassから派生するChildClassScriptableObjectになるはずです。

C#

1using UnityEngine; 2 3public class ChildClass : RootClass 4{ 5 [SerializeField] protected int additionalValue = 24; 6 public int AdditionalValue => this.additionalValue; 7 8 public override void Act() 9 { 10 base.Act(); 11 Debug.Log("This is ChildClass. 1day is " + this.additionalValue + " hour. " + this.exampleValue); 12 } 13}

「スクリプトファイルを差し替えることでデータの型を変更する」という挙動を実現する上では、これらデータクラスは1ファイル1クラスとし、クラス名とファイル名を一致させておいた方が好都合かと思います。

DataClassの方は下記のようにしてみました。

C#

1using UnityEngine; 2#if UNITY_EDITOR 3using UnityEditor; 4#endif 5 6[CreateAssetMenu(fileName = "parent", menuName = "CreateParent")] 7public class DataClass : ScriptableObject 8{ 9 [SerializeField, HideInInspector] private RootClass rootClassData; 10 11 public RootClass RootClassData 12 { 13 get => this.rootClassData; 14 protected set => this.rootClassData = value; 15 } 16 17 public virtual void Act(GameObject target) 18 { 19 this.RootClassData.Act(); 20 } 21} 22 23#if UNITY_EDITOR 24[CustomEditor(typeof(DataClass))] 25public class DataClassEditor : Editor 26{ 27 private const string DataAssetName = "Data"; 28 private const HideFlags DataAssetHideFlags = HideFlags.HideInHierarchy; 29 private const string DataReferenceFieldName = "rootClassData"; 30 31 private Editor dataEditor; 32 33 public override void OnInspectorGUI() 34 { 35 this.serializedObject.Update(); 36 37 // まだアセットが永続化されていないならスキップする 38 if (!EditorUtility.IsPersistent(this.target)) 39 { 40 EditorGUILayout.HelpBox("Object is not persistent yet.", MessageType.Info); 41 return; 42 } 43 44 // まずデータオブジェクトプロパティを探し... 45 var dataProperty = this.serializedObject.FindProperty(DataReferenceFieldName); 46 if (dataProperty == null) 47 { 48 EditorGUILayout.HelpBox($"{DataReferenceFieldName} not found.", MessageType.Error); 49 return; 50 } 51 52 // そして現在参照しているデータオブジェクトを取得する 53 var data = dataProperty.objectReferenceValue as RootClass; 54 if (data == null) 55 { 56 // アセット生成直後はデータオブジェクトが存在しないので生成してやる 57 data = CreateInstance<RootClass>(); 58 data.name = DataAssetName; 59 data.hideFlags = DataAssetHideFlags; 60 AssetDatabase.AddObjectToAsset(data, this.target); 61 dataProperty.objectReferenceValue = data; 62 this.serializedObject.ApplyModifiedProperties(); 63 AssetDatabase.SaveAssets(); 64 } 65 66 // データの型と対応するスクリプトアセットを提示するフィールドを表示する 67 // スクリプトアセットを差し替えることでデータの型を変更できるようにする 68 var dataScript = MonoScript.FromScriptableObject(data); 69 var newDataScript = EditorGUILayout.ObjectField("Data Type Script", dataScript, typeof(MonoScript), false) as MonoScript; 70 if ((newDataScript != null) && (newDataScript != dataScript)) 71 { 72 var newDataType = newDataScript.GetClass(); 73 if (!typeof(RootClass).IsAssignableFrom(newDataType)) 74 { 75 EditorUtility.DisplayDialog( 76 "Invalid Type", 77 $"Type {newDataType} is not compatible with type {typeof(RootClass)}.", 78 "OK"); 79 } 80 else 81 { 82 // データの型が変わったので、データオブジェクトを差し替える 83 // まず新しい型のデータオブジェクトを作り、なるべくデータを引き継がせる 84 var newData = CreateInstance(newDataType) as RootClass; 85 newData.name = DataAssetName; 86 newData.hideFlags = DataAssetHideFlags; 87 EditorUtility.CopySerializedManagedFieldsOnly(data, newData); 88 AssetDatabase.AddObjectToAsset(newData, this.target); 89 Undo.RegisterCreatedObjectUndo(newData, $"Create {newDataType}"); 90 91 // データオブジェクトへの参照を更新し、古いデータオブジェクトは削除する 92 Undo.RecordObject(this.target, "Change Data Type"); 93 dataProperty.objectReferenceValue = newData; 94 Undo.DestroyObjectImmediate(data); 95 data = newData; 96 this.serializedObject.ApplyModifiedProperties(); 97 AssetDatabase.SaveAssets(); 98 } 99 } 100 101 // 現在のデータの型と対応するエディターを取得し、それを使ってデータオブジェクトの内容を編集する 102 CreateCachedEditor(data, null, ref this.dataEditor); 103 if (this.dataEditor == null) 104 { 105 EditorGUILayout.HelpBox($"Editor for {data.GetType()} not found.", MessageType.Error); 106 return; 107 } 108 this.dataEditor.OnInspectorGUI(); 109 } 110} 111#endif

できあがったアセットファイルをテキストエディタで開いてみたところ、下記のようにデータオブジェクトがサブアセットとして埋め込まれ、メインアセットのフィールドはそれを参照する形になっていました。データ型としてはChildClassを選択した状態です。

YAML

1%YAML 1.1 2%TAG !u! tag:unity3d.com,2011: 3--- !u!114 &-2057368281287218590 4MonoBehaviour: 5 m_ObjectHideFlags: 1 6 m_CorrespondingSourceObject: {fileID: 0} 7 m_PrefabInstance: {fileID: 0} 8 m_PrefabAsset: {fileID: 0} 9 m_GameObject: {fileID: 0} 10 m_Enabled: 1 11 m_EditorHideFlags: 0 12 m_Script: {fileID: 11500000, guid: ce435bc9ccec7aa4991b478d10e740b0, type: 3} 13 m_Name: Data 14 m_EditorClassIdentifier: 15 exampleValue: Over. 16 additionalValue: 24 17--- !u!114 &11400000 18MonoBehaviour: 19 m_ObjectHideFlags: 0 20 m_CorrespondingSourceObject: {fileID: 0} 21 m_PrefabInstance: {fileID: 0} 22 m_PrefabAsset: {fileID: 0} 23 m_GameObject: {fileID: 0} 24 m_Enabled: 1 25 m_EditorHideFlags: 0 26 m_Script: {fileID: 11500000, guid: 7fd9314a767208b458d58f01860249c0, type: 3} 27 m_Name: Data 28 m_EditorClassIdentifier: 29 rootClassData: {fileID: -2057368281287218590} 30

投稿2020/06/05 21:42

Bongo

総合スコア10807

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

kaitorisenkou

2020/06/10 05:48

やりたかった事にかなり近い回答です!ありがとうございます。
guest

0

いまいちどう実現したいかの形がわからなかったのでとりあえずエラーが出ずに RootClass の項目が出るようにしてみました
(編集はできるようになっているかと思います)
RootClass側には特に修正は加えていません

using System; using UnityEngine; #if UNITY_EDITOR using UnityEditor; #endif [System.Serializable] [CreateAssetMenu(fileName = "parent", menuName = "CreateParent")] public class DataClass : ScriptableObject { [SerializeField, HideInInspector] string className = null; public string ClassName => className; [SerializeField, HideInInspector] RootClass rootClassData = null; public RootClass RootClassData => rootClassData; virtual public void Act(GameObject target) { rootClassData.Act(); } public void SetClassName(string className) { this.className = className; rootClassData = null; } public void SetParentClass(RootClass parentClass) { this.rootClassData = parentClass; } } #if UNITY_EDITOR [CustomEditor(typeof(DataClass))] public class DataClassEditor : Editor { /* private void OnEnable() { } */ DataClass targetDC => ((DataClass)target); public override void OnInspectorGUI() { serializedObject.Update(); TextAsset behaviourScript; string className; behaviourScript = EditorGUILayout.ObjectField("BehaviourScript", null, typeof(TextAsset), false) as TextAsset; if (behaviourScript != null) { className = behaviourScript.name; } else { className = targetDC.ClassName; } if (className == null) { EditorGUILayout.HelpBox("ClassName is null.", MessageType.Error); } else { if (className == "") { EditorGUILayout.HelpBox("ClassName is blank.", MessageType.Error); } else { Type type = Type.GetType(className); if (behaviourScript != null) targetDC.SetClassName(behaviourScript.name); EditorGUILayout.HelpBox("Attached: " + className, MessageType.Info); var serObj = new SerializedObject(this.targetDC); SerializedProperty prop = serObj.FindProperty("rootClassData"); while (prop.Next(true)) { EditorGUILayout.PropertyField(prop); } serObj.ApplyModifiedProperties(); } } } } #endif

投稿2020/06/01 17:48

MMashiro

総合スコア2378

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

kaitorisenkou

2020/06/02 00:30

回答いただいた方法でインスペクタ編集はできるようになりましたが、子クラスを選択した場合にその編集項目が追加されませんでした。 また、「Array」という謎のint型配列が表示されるようになっています。 質問内容を編集させて頂きました。意図がわかりにくく申し訳ありません。
MMashiro

2020/06/04 09:43

私の知識不足の可能性もありますが、Unity標準の機能だけでは親クラスだけでなく子クラスの要素もインスペクタに列挙してかつScriptableObjectとして保存するのはかなり厳しいのではないかと思います (独自のアセットを定義するのであればまた別ですが…)
guest

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

15分調べてもわからないことは
teratailで質問しよう!

ただいまの回答率
85.48%

質問をまとめることで
思考を整理して素早く解決

テンプレート機能で
簡単に質問をまとめる

質問する

関連した質問