Reputation: 23
I am facing a problem when adding elements to a SerializeReference custom class array. For some reason everything works fine but when adding a new element to the array from Editor the following assertion pops out:
I leave the code below:
namespace Editor
{
[CustomPropertyDrawer(typeof(BaseClass))]
public class SubclassPropertyDrawer : PropertyDrawer
{
public override VisualElement CreatePropertyGUI(SerializedProperty property)
{
Debug.Log($"{property.displayName} managedReferenceValue is {property.managedReferenceValue}");
var root = new VisualElement();
if (property.managedReferenceValue == null)
{
property.managedReferenceValue = Activator.CreateInstance(typeof(DerivedClass1));
property.serializedObject.ApplyModifiedProperties();
}
var foldout = new Foldout();
foldout.text = property.displayName;
var enumerator = property.Copy().GetEnumerator();
enumerator.MoveNext();
var childProperty = enumerator.Current as SerializedProperty;
var propertyField = new PropertyField(childProperty, childProperty.displayName);
foldout.Add(propertyField);
root.Add(foldout);
return root;
}
}
}
And the ScriptableObject here:
namespace Test
{
[CreateAssetMenu(fileName = "Test", menuName = "ScriptableObjects/Test", order = 1)]
public class TestSO : ScriptableObject
{
[SerializeReference]
public BaseClass[] customClass;
}
}
Any idea on what I am doing wrong?
Thank you in advance!
I am trying to basically display a Dropdown element that allows me to select between the different derived types of a base class and show its fields.
i would expect this to work properly without any error.
Upvotes: 1
Views: 728
Reputation: 90813
Tbh I couldn't reproduce your exact issue regarding the exception.
There are however a couple of other pitfalls I came across while trying ;)
Assuming your BaseClass
is not derived from UnityEngine.Object
- in which case you shouldn't be using SerializeReference
at all.
Main issue with CreatePropertyGUI
vs OnGUI
is
OnGUI
is called every frame. This sometimes adds quite some overhead regarding getting properties / initializing states etc. On the other hand it sometimes makes things easier with refreshing and changesCreatePropertyGUI
is only called once when the Inspector is loaded + on certain change events (e.g. reordering elements in a list). This caused an issue with forcing the foldout to re-layout after changing the type (see code below).Then also you probably wanted to include the type selection popup itself. You can use the TypeCache
to list them (This could then even be used in a more generic way via a property attribute.)
This is the inspector I came up with
[CustomPropertyDrawer(typeof(BaseClass))]
public class SubclassPropertyDrawer : PropertyDrawer
{
public override VisualElement CreatePropertyGUI(SerializedProperty property)
{
// Get all types derived from BaseClass
var types = TypeCache.GetTypesDerivedFrom<BaseClass>().OrderBy(t => t.FullName).ToList();
// Get the current type of the property
var currentType = property.managedReferenceValue?.GetType();
var root = new VisualElement();
var foldout = new Foldout
{
text = property.displayName
};
// Dropdown to select the type
var typeSelectionPopup = new PopupField<Type>("Type", types, types.IndexOf(currentType), TypeDisplay, TypeDisplay);
typeSelectionPopup.RegisterCallback<ChangeEvent<Type>>(ev => OnNewTypeSelected(ev.newValue));
// If the property is null, set it to the first available type
if (property.managedReferenceValue == null)
{
OnNewTypeSelected(types[0]);
}
root.Add(foldout);
RefreshProperties();
return root;
string TypeDisplay(Type type)
{
// optional: Group types by namespace into submenus
return type?.FullName == null ? string.Empty : type.FullName.Replace('.', '/');
}
void OnNewTypeSelected(Type newType)
{
var newObject = newType == null ? null : Activator.CreateInstance(newType);
property.managedReferenceValue = newObject;
property.serializedObject.ApplyModifiedProperties();
// also need to refresh the property fields - otherwise they will still show the old fields
RefreshProperties();
}
void RefreshProperties()
{
foldout.Clear();
// per default add the type selection popup
foldout.Add(typeSelectionPopup);
var iterator = property.Copy().GetEnumerator();
var childDepth = property.depth + 1;
while (iterator.MoveNext())
{
// only iterate first level children
// otherwise e.g. for a Vector 2 you would first get the vector itself and then the nested "x" and "y" fields
// also for arrays/lists this would iterate each item individually
// we will want to assume these should rather use their very own drawers
if (iterator.Current is not SerializedProperty childProperty || childProperty.depth != childDepth)
continue;
var propertyField = new PropertyField(childProperty);
// This is required for dynamically created UI items so they are correctly linked to the serializedObject
// For UI items created in CreatePropertyGUI Unity does this internally
propertyField.Bind(property.serializedObject);
foldout.Add(propertyField);
}
(iterator as IDisposable)?.Dispose();
}
}
}
And here some example types to select from
namespace Tests2
{
[Serializable]
public class DerivedClass4 : BaseClass
{
public Vector3 someVector3;
}
[Serializable]
public class DerivedClass5 : BaseClass
{
public Texture2D someTexture;
public List<int> someList;
}
}
namespace Tests
{
[Serializable]
public class BaseClass
{
}
[Serializable]
public class DerivedClass1 : BaseClass
{
public float someFloat;
}
[Serializable]
public class DerivedClass2 : BaseClass
{
public int someInt;
public string someString;
}
public class DerivedClass3 : BaseClass
{
[Range(-2, 6)]
public float someFloatRange;
public Vector2 someVector2;
}
}
In general: Have in mind that with this - going by serialized type names - you will run into trouble whenever you rename the types, namespace, or assembly!
Upvotes: 2