- Home /
Calling a derived class (Serialized in a Scriptable Object) returns the super class.
Greetings!
There is a large chunk of source code below, maybe its to much and someone who potentially has a useful input can be scared away so: My problem is the following, when I use the TestX(:MonoBehaviour) script everything works fine, I get the correct types back - but the EditorWindow fails to do so and returns IDatabaseType (Base Class) for the derived classes! I suspect that it is related to that TestX.cs calls everything from a monobehaviour while my database is using an ScriptableObject its base.
Below you see the class which the database types will be derived from, the classes have been marked as serializable for a reason (Storing together with a Scriptable Object.)
[Serializable]
public class IDatabaseType
{
[SerializeField]
private object value = new object();
[SerializeField]
public virtual object GetValue()
{
return null;
}
[SerializeField]
public virtual void SetValue(object newValue)
{
}
}
Here is one of the four derived classes (All four are identical ignoring the minor differences of variable types.)
[Serializable]
public class DBString : IDatabaseType
{
[SerializeField]
private string value;
public DBString(string value)
{
SetValue(value);
}
public override object GetValue()
{
return value;
}
public override void SetValue(object newValue)
{
value = (string)newValue;
}
}
Using these five (IDatabaseType + 4 Derivations) classes I have no problems, I can properly access to the derived classes by calling them like this.
public class Testx : MonoBehaviour {
// Use this for initialization
void Start () {
List<IDatabaseType> x = new List<IDatabaseType>();
x.Add(new Unity.Database.DBBoolean(false));
x.Add(new Unity.Database.DBFloat(0f));
x.Add(new Unity.Database.DBInt(0));
x.Add(new Unity.Database.DBString(""));
IDatabaseType DBBoolean = new Unity.Database.DBBoolean(false);
IDatabaseType DBFloat = new Unity.Database.DBFloat(0f);
Debug.Log("(1) one by one: " + x[0].GetType());
Debug.Log("(2) one by one: " + x[1].GetType());
Debug.Log("(3) one by one: " + x[2].GetType());
Debug.Log("(4) one by one: " + x[3].GetType());
foreach(IDatabaseType IDT in x)
{
Debug.Log("foreach " + IDT.GetType());
}
Debug.Log("Unique: " + DBBoolean.GetType());
Debug.Log("Unique: " + DBFloat.GetType());
}
}
However! it gets tricky when the Scriptable Object comes into the game, while the database gets properly serialized after the serialization it fails to properly identify the classes! It no longer returns the derived class but the base class!
The Database (I removed some of the method bodies to rise visibility.) here.
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System;
[Serializable]
public class UnityDatabase : ScriptableObject {
[SerializeField]
private string DatabaseName;
[SerializeField]
private int DatabaseID;
[SerializeField]
public IDatabaseType[] Header;
[SerializeField]
public string[] HeaderLabel = new string[0];
[SerializeField]
public List<DatabaseRow> data = new List<DatabaseRow>();
/// <summary>
/// Setup new Database
/// </summary>
/// <param name="Header">Database Type Setup (Headline)</param>
public UnityDatabase(IDatabaseType[] _Header, string[] _HeaderLabel)
{
Header = _Header;
HeaderLabel = _HeaderLabel;
}
/// <summary>
/// Create new Entry
/// </summary>
/// <param name="withValues">Values to use</param>
public void CreateNewEntry(params IDatabaseType[] withValues)
{
// Verify given values
if(!verifyRow(withValues))
{
return;
}
// Create Database Entry
data.Add(new DatabaseRow(withValues));
//Debug.Log("[SYSTEM]: Created new Row.");
}
/// <summary>
/// Get Specified Entry
/// </summary>
/// <returns></returns>
public IDatabaseType GetValue(int row, int column)
{
return data[row].data[column];
}
/// <summary>
/// Get Specified Row
/// </summary>
/// <returns></returns>
public DatabaseRow GetValue(int row)
{
return data[row];
}
/// <summary>
/// Overwrite Targetted Field
/// </summary>
/// <param name="newValue">New Value</param>
/// <param name="row">Row to target</param>
/// <param name="column">Column to target</param>
public void SetValue(IDatabaseType newValue, int row, int column)
{
// Verify if target point exists
try
{
IDatabaseType x = data[row].data[column];
} catch
{
Debug.Log("Outside Index Range.");
return;
}
if(verifyEntry(newValue, data[row].data[column]))
{
data[row].data[column] = newValue;
}
}
/// <summary>
/// Verify if given data is compatible with this databases header
/// </summary>
/// <param name="needVerification">Data to Verify</param>
/// <returns></returns>
private bool verifyRow(IDatabaseType[] needVerification)
{
// Verify Length
if (Header.Count() != needVerification.Count())
{
//Debug.Log("[SYSTEM]: Length Failure.");
return false;
}
// Verify Types
bool typeSuccess = true;
for (int i = 0; i < Header.Length; i++)
{
// Check if TypeOf is equal
if (!verifyEntry(needVerification[i], Header[i]))
{
//Debug.Log("[SYSTEM]: Type Failure.");
typeSuccess = false;
}
}
return typeSuccess;
}
private bool verifyEntry(IDatabaseType first, IDatabaseType second)
{
if(first.GetType() != second.GetType())
Debug.Log("Warning: The given Entry is not compatible with the Signature!");
return first.GetType() == second.GetType();
}
}
[Serializable]
public class DatabaseRow
{
[SerializeField]
public List<IDatabaseType> data = new List<IDatabaseType>();
public DatabaseRow(params IDatabaseType[] RowSetup)
{
data = RowSetup.ToList();
}
}
The Database is read from a EditorWindow.
public class EditorDatabaseManager : EditorWindow
{
[MenuItem("Database/Database Editor")]
public static void ShowWindow()
{
EditorWindow.GetWindow(typeof(EditorDatabaseManager));
}
private UnityDatabase db;
void OnGUI()
{
db = (UnityDatabase)EditorGUILayout.ObjectField(db, typeof(UnityDatabase), true);
if (db == null)
return;
if (GUILayout.Button("Create new Row"))
{
db.CreateNewEntry(
db.Header
);
}
if (db.data == null)
{
EditorGUILayout.HelpBox("Data Null.", MessageType.Warning);
return;
}
int x = 0;
foreach (DatabaseRow DR in db.data)
{
EditorGUILayout.BeginHorizontal();
EditorGUILayout.LabelField("" + x++, GUILayout.Width(50f));
for (int i = 1; i < DR.data.Count; i++)
{
Type t = db.Header[i - 1].GetType().GetElementType();
IDatabaseType IDT = db.Header[i - 1];
EditorGUILayout.LabelField(IDT.GetType().ToString());
}
EditorGUILayout.EndHorizontal();
}
}
}
To create the asset object you would also need this fellow here:
public class CreateDatabaseManager : EditorWindow
{
[MenuItem("Database/Database Creator")]
public static void ShowWindow()
{
EditorWindow.GetWindow(typeof(CreateDatabaseManager));
}
private Unity.Database.DBTypes[] header = new Unity.Database.DBTypes[0];
private string[] labels = new string[0];
private int headerLength = 0;
private string dbname = "Undefined";
void OnGUI()
{
// Verify header length
if (header.Length != headerLength || labels.Length != headerLength) {
header = new Unity.Database.DBTypes[headerLength];
labels = new string[headerLength];
for(int i = 0; i < headerLength; i++)
{
labels[i] = "" + i;
}
OnGUI();
return;
}
// Parameters
EditorGUILayout.BeginHorizontal();
headerLength = EditorGUILayout.IntField("Header Index Length: ", headerLength);
dbname = EditorGUILayout.TextField(dbname);
EditorGUILayout.EndHorizontal();
EditorGUILayout.Space();
// Label Definition
EditorGUILayout.BeginHorizontal();
for (int i = 0; i < labels.Length; i++)
{
labels[i] = EditorGUILayout.TextField(labels[i]);
}
EditorGUILayout.EndHorizontal();
// Type Definition
EditorGUILayout.BeginHorizontal();
for (int i = 0; i < headerLength; i++)
{
header[i] = (Unity.Database.DBTypes)EditorGUILayout.EnumPopup(header[i]);
}
EditorGUILayout.EndHorizontal();
EditorGUILayout.Space();
// Create on Click
if (GUILayout.Button("Create new Table (" + dbname + ")"))
{
// Prepare Object
UnityDatabase obj = new UnityDatabase(ConvertToHeader(header), labels);
dbname = (dbname.Equals("")) ? "Undefined" : dbname;
AssetDatabase.CreateAsset(obj, AssetDatabase.GenerateUniqueAssetPath("Assets/" + dbname + ".asset"));
AssetDatabase.SaveAssets();
AssetDatabase.Refresh();
EditorUtility.FocusProjectWindow();
Selection.activeObject = obj;
header = new Unity.Database.DBTypes[0];
headerLength = 0;
dbname = "Undefined";
}
}
IDatabaseType[] ConvertToHeader(Unity.Database.DBTypes[] target)
{
List<IDatabaseType> db = new List<IDatabaseType>();
foreach(Unity.Database.DBTypes DBT in target)
{
switch(DBT)
{
case Unity.Database.DBTypes.Bool:
db.Add(new Unity.Database.DBBoolean(false));
break;
case Unity.Database.DBTypes.Float:
db.Add(new Unity.Database.DBFloat(0f));
break;
case Unity.Database.DBTypes.Int:
db.Add(new Unity.Database.DBInt(0));
break;
case Unity.Database.DBTypes.String:
db.Add(new Unity.Database.DBString(""));
break;
}
}
return db.ToArray();
}
}
Now my problem is the following, when I use the TestX.cs script everything works fine, I get the correct types back - but the editorwindow fails to do so and returns IDatabaseType for the derived classes! I suspect that it is related to that TestX.cs calls everything from a monobehaviour while my database is using an ScriptableObject as a base.
Edit: The namespaces you should use with this are the following:
using UnityEngine;
using UnityEditor;
using System.Collections;
using System;
using System.Linq;
using System.Collections.Generic;
a quick update, It isnt because of the DatabaseRow, a bit of experimenting around with the below attached source (Testx.cs) showed that the class itself works fine.
public class Testx : $$anonymous$$onoBehaviour {
void Start () {
List<IDatabaseType> x = new List<IDatabaseType>();
x.Add(new Unity.Database.DBBoolean(false));
x.Add(new Unity.Database.DBFloat(0f));
x.Add(new Unity.Database.DBInt(0));
x.Add(new Unity.Database.DBString(""));
List<DatabaseRow> y = new List<DatabaseRow>();
y.Add(new DatabaseRow(x.ToArray()));
foreach (DatabaseRow DR in y)
{
foreach(IDatabaseType IDT in DR.data)
{
Debug.Log("Nested: " + IDT.GetType());
}
}
}
}
Another little update, by creating a copy of UnityDatabase where it doesnt inherit from ScriptableObject and calling the entries again I got the proper types. I am really getting suspicious of that ScriptableObject fellow now...
string[] s = new string[4];
s[0] = "0";
s[0] = "1";
s[0] = "2";
s[0] = "3";
UnityDatabase1 UNIDB = new UnityDatabase1(x.ToArray(), s);
UNIDB.CreateNewEntry(x.ToArray());
foreach (DatabaseRow DR in UNIDB.data)
{
foreach(IDatabaseType IDT in DR.data)
{
Debug.Log(IDT.GetType());
}
}
Answer by Bunny83 · Aug 21, 2016 at 11:23 PM
I answered the same question a couple of times now, so just read them over here:
http://answers.unity3d.com/answers/735548/view.html
http://answers.unity3d.com/answers/245769/view.html
Unfortunately Unity doesn't support inheritance in custom classes when it comes to serialization
Thank you very much!
Of course that's only true for Unity's serialization system. The normal .NET serialization (binary formatter and the like) would work as expected. Unfortunately some crucial types in Unity (Vector2/3/4, Quaternion, Color, Color32, Rect, ...) are not marked as serializable and would need some special handling.
Unity now has two special helper classes (like the BinaryWriter / Reader) inside the Networking sub-namespace called NetworkReader / NetworkWriter. Of course they are designed for serializing data that should be send over a network connection, but they can be used to serialize most Unity types into a byte array. Of course only pure data types / structs.