Context: I’m trying to implement a skill system using inheritance. Currently, I have an abstract class (SkillBase) from which other skills are derived. Every character on the scene has a list of SkillBase (SkillList). In practice this works fine for player controlled characters as skills are added or loaded via scripts.
Issue: Unity editor doesn’t support abstract class exposure in the inspector. So filling/editing the list is not easy when I want to make a unique npc.
My current leads are:
1 make a custom editor tool for maintaining skill lists on characters
2 looking into property drawer to do the same but hopefully retaining unity’s built in inspector functionality
3 redesign the skill system, add monobehaviour to skillbase, have each character have a child object Skills to attach skills to it.
It will be slightly different for collections, where you need to work through the serialized property of the collection, and assign to the managed value of the child serialized properties (the elements of the collection).
There are various editor extensions out there that attempt to make this reusable.
To be fair, the options that open up with [SerializeReference] are too good to pass up. I would recommend everyone give the attribute/the serialization it allows a go and explore what it allows.
Thanks for the rabbit hole I just went down. I can’t stand having to make non-generic property drawers so here’s a generic one. GPT-4o was a huge help but as usual it was very obnoxious with how it liked to rename things when I asked for advice. Everything appears to be serializing correctly but I haven’t tested it enough.
Attribute
using System;
using UnityEngine;
[AttributeUsage(AttributeTargets.Field, Inherited = true, AllowMultiple = false)]
public class SerializeReferenceDrawer : PropertyAttribute {}
Property Drawer
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using UnityEditor;
using UnityEngine;
[CustomPropertyDrawer(typeof(SerializeReferenceDrawer))]
public class SerializeReferencePropertyDrawer : PropertyDrawer
{
private static Dictionary<Type, Type[]> _derivedTypesCache = new Dictionary<Type, Type[]>();
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
{
if (property.propertyType != SerializedPropertyType.ManagedReference)
{
EditorGUI.PropertyField(position, property, label);
return;
}
EditorGUI.BeginProperty(position, label, property);
Type baseType = GetFieldType(property);
if (baseType == null)
{
EditorGUI.PropertyField(position, property, label);
EditorGUI.EndProperty();
return;
}
if (!_derivedTypesCache.TryGetValue(baseType, out var derivedTypes))
{
derivedTypes = GetDerivedTypes(baseType).ToArray();
_derivedTypesCache[baseType] = derivedTypes;
}
Type currentType = property.managedReferenceValue?.GetType();
string currentTypeName = currentType?.Name ?? "Null";
List<string> typeNames = derivedTypes.Select(t => t.Name).ToList();
typeNames.Insert(0, "Null");
Rect popupRect = new Rect(position.x, position.y, position.width, EditorGUIUtility.singleLineHeight);
int currentIndex = typeNames.IndexOf(currentTypeName);
int newIndex = EditorGUI.Popup(popupRect, label.text, currentIndex, typeNames.ToArray());
if (newIndex != currentIndex)
{
if (newIndex == 0)
{
property.managedReferenceValue = null;
}
else
{
Type newType = derivedTypes[newIndex - 1];
property.managedReferenceValue = Activator.CreateInstance(newType);
}
property.serializedObject.ApplyModifiedProperties();
}
if (property.managedReferenceValue != null)
{
EditorGUI.indentLevel++;
SerializedProperty iterator = property.Copy();
SerializedProperty endProperty = iterator.GetEndProperty();
bool enterChildren = true;
while (iterator.NextVisible(enterChildren) && !SerializedProperty.EqualContents(iterator, endProperty))
{
EditorGUILayout.PropertyField(iterator, true);
enterChildren = false;
}
EditorGUI.indentLevel--;
}
EditorGUI.EndProperty();
}
private static Type GetFieldType(SerializedProperty property)
{
string[] paths = property.propertyPath.Split('.');
Type type = property.serializedObject.targetObject.GetType();
for (int i = 0; i < paths.Length; i++)
{
if (paths[i] == "Array")
{
i++;
type = type.GetElementType() ?? type.GetGenericArguments()[0];
}
else
{
FieldInfo field = type.GetField(paths[i], BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
if (field == null) return null;
type = field.FieldType;
}
}
return type;
}
private static IEnumerable<Type> GetDerivedTypes(Type baseType)
{
return AppDomain.CurrentDomain.GetAssemblies()
.SelectMany(assembly => assembly.GetTypes())
.Where(type => baseType.IsAssignableFrom(type) && !type.IsAbstract && type != baseType);
}
}
Example
using System;
using UnityEngine;
public class Example : MonoBehaviour
{
[SerializeReference, SerializeReferenceDrawer]
public ISkill skill;
[SerializeReference, SerializeReferenceDrawer]
public Animal animal;
}
public interface ISkill
{
public void Activate();
}
public class AttackSkill : ISkill
{
public int damage = 0;
public void Activate() {}
}
public class DefendSkill : ISkill
{
public int defense = 0;
public void Activate() {}
}
public abstract class Animal
{
public bool hasLegs;
}
public class Cat : Animal
{
public int miceCaught;
}
public class Dog : Animal
{
public int barksToday;
}
Yes, it’s a functional prototype at best. I can’t blame it entirely though. Any time I saw code that I didn’t recognize immediately (and didn’t feel like looking up) I regenerated the result.
I’ll give that try. I for some reason thought that it had to be an abstract class for inheritance and make a base class list of. Should work fine since I think I can still override methods i put in the base class
any use cases in particular?
Edit: So use case is for this, Sorry didnt see your previous post for some reason, this might be what i needed. Gonna dig into it and try it out, but taking a preemptive guess… is it in essence exposing a reference to the fields/properties within the abstract class instead of exposing the abstract class directly?
Well it enables by-refence serialization. So base class/interface and derived type serialisation is the main use that anyone will use it for.
But it also allows you to build and serialise non-linear data structures; tree-like structures and node-based ones being the first ones to come to mind. No doubt a bunch more, of course.
And there’s tons of use-cases for all the above there.
You can do either/or/both. Just depends on the use case. Neither is strictly correct. My example didn’t expose it and just invoked a method on it via another method.
Note that the custom editor doesn’t care in either case.
I should look into making a generic property drawer myself. I’ve never needed to due to owning Odin Inspector.
Thanks for the example. Tried going with a non - abstract class, and it does expose it in inspector. But it didn’t go as planned. Couldn’t just drag and drop a skill class into it to add elements. So it seems custom editor / property drawer is the way to go for my use case.
Your scripts are just text files. Aside from Monobehaviours and scriptable objects they can have any number of classes inside them. They don’t actually represent your compiled code or their data. Working with subclass serialisation is always going to involve some kind of dropdown.
Also there was nothing in Ryiah’s example that involved collections.
It’s also worth mentioning, that you could also make the abstract class derive from ScriptableObject (or MonoBehaviour), and then Unity would be able to serialize and visualize a polymorphic list of references to instances of it in the Inspector.
Like @SisusCo mentioned you need to inherit from ScriptableObject. In my example once you’ve done that you can create additional script files with types that have [CreateAssetMenu] and it’s suddenly drag-and-droppable.
I’m still waking up but if I understand this line correctly it appears to be working (I made AttackSkill use an array and DefendSkill use a List, and the skills entry in the MonoBehaviour an array).
Edit: Oh, it only works for the interface not the abstract class. I really need to rework the drawer by hand.
Right here’s a decent generic/reusable example using UI Toolkit (because I’m buggered if I’m going to do this with tired old IMGUI).
The Attribute
using System;
using UnityEngine;
[System.Diagnostics.Conditional("UNITY_EDITOR")]
[AttributeUsage(AttributeTargets.Field, Inherited = true, AllowMultiple = false)]
public class SubclassSelectorAttribute : PropertyAttribute { }
The Drawer
#if UNITY_EDITOR
using System;
using System.Collections;
using System.Collections.Generic;
using System.Reflection;
using UnityEngine;
using UnityEngine.UIElements;
using UnityEditor;
using UnityEditor.UIElements;
[CustomPropertyDrawer(typeof(SubclassSelectorAttribute))]
public sealed class SubclassSelectorPropertyDrawer : PropertyDrawer
{
#region Overrides
public override VisualElement CreatePropertyGUI(SerializedProperty property)
{
var visualElement = new VisualElement();
var propertyField = new PropertyField();
propertyField.BindProperty(property);
propertyField.label = " ";
if (property.propertyType != SerializedPropertyType.ManagedReference)
{
visualElement.Add(propertyField);
return visualElement;
}
var types = GetTypes(fieldInfo, property);
var dropdownField = new TypePopupField(property, types);
visualElement.Add(dropdownField);
visualElement.Add(propertyField);
return visualElement;
}
#endregion
#region Internal Methods
private static bool IsCollection(Type fieldType)
{
if (fieldType.IsArray == true)
{
return true;
}
if (fieldType.IsGenericType && fieldType.GetGenericTypeDefinition() == typeof(List<>))
{
return true;
}
return false;
}
private static List<Type> GetTypes(FieldInfo fieldInfo, SerializedProperty property)
{
var value = property.managedReferenceValue;
Type currentType = value?.GetType();
Type fieldType = fieldInfo.FieldType;
Type baseType;
bool isCollection = IsCollection(fieldType);
var types = new List<Type>()
{
currentType,
null
};
if (fieldType.IsAbstract == false && isCollection == false)
{
types.Add(fieldType);
}
if (isCollection == true)
{
baseType = fieldType.GetGenericArguments()[0];
if (baseType.IsAbstract == false)
{
types.Add(baseType);
}
}
else
{
baseType = fieldType;
}
var derivedTypes = TypeCache.GetTypesDerivedFrom(baseType);
foreach (var derivedType in derivedTypes)
{
types.Add(derivedType);
}
return types;
}
#endregion
#region Internal Types
public sealed class TypePopupField : PopupField<Type>
{
#region Internal Members
private readonly SerializedProperty _property;
#endregion
public TypePopupField(SerializedProperty property, List<Type> types) : base(property.displayName, types, 0, GetTypeName, GetTypeName)
{
_property = property;
this.RegisterValueChangedCallback(OnValueSelected);
}
#region Internal Methods
private void OnValueSelected(ChangeEvent<Type> changeEvent)
{
Type selectedType = changeEvent.newValue;
if (selectedType == null)
{
_property.managedReferenceValue = null;
_property.serializedObject.ApplyModifiedProperties();
}
else
{
var constructor = selectedType.GetConstructor(Type.EmptyTypes);
if (constructor != null)
{
var value = constructor.Invoke(null);
_property.managedReferenceValue = value;
_property.serializedObject.ApplyModifiedProperties();
}
else
{
Debug.LogWarning($"Selected Type {selectedType.Name} does not have a parameterless constructor. Cannot assign instance of type.");
}
}
}
private static string GetTypeName(Type type)
{
if (type == null)
{
return "Null";
}
else
{
return ObjectNames.NicifyVariableName(type.Name);
}
}
#endregion
}
#endregion
}
#endif
An Example
using System.Collections.Generic;
using UnityEngine;
public class SubclassSelectorExample : MonoBehaviour
{
#region Inspector Fields
[SerializeReference, SubclassSelector]
private IBaseInterface _baseInterface = null;
[SerializeReference, SubclassSelector]
private BaseClass _classBase = null;
[SerializeReference, SubclassSelector]
private List<BaseClass> _classes = new();
#endregion
#region Properties
public IBaseInterface BaseInterface => _baseInterface;
public BaseClass ClassBase => _classBase;
public List<BaseClass> Classes => _classes;
#endregion
#region Types
public interface IBaseInterface { }
[System.Serializable]
public class BaseClass : IBaseInterface { }
[System.Serializable]
public class ClassA : BaseClass
{
public string ClassAString;
}
[System.Serializable]
public class ClassB : BaseClass
{
public float ClassBFLoat;
}
[System.Serializable]
public class ClassC : ClassA
{
public int ClassCInt;
}
#endregion
}
And looks pretty meh given the nature of using property fields:
This was not without frustrations and I was regularly missing Odin Inspector’s superior API the entire time. Property Drawers are pain, and DropdownField/PopupField are stupidly designed visual elements. But we still got there in the end and it’s better than whatever ChatGPT farted out (no offence Ryiah).
Note: We can’t do custom drawers for collections so it always adds null as the value of new element.
Right, the idea did cross my mind when i was initially designing the skill class.
I didn’t do monobehaviour because i wanted something more data oriented, since my characters aren’t always going to be gameobjects. Sometimes they’re just a list of characters in a SO asset. Though i suppose it’s entirely possible to have them always be gameobjects but just off screen in non gameplay scenes. The other thing is skills need to be serializable, since a characters skills can change. Monobehaviour just didn’t seem to offer the flexibility i needed.
I chose not to do scriptable object for the skill class since to my understanding they are purpose built to be data containers. If every skill did a similar thing and just used different values this would be fine. But i wanted an abstract UseSkill method, that each skill inheriting from skillbase could define uniquely. I think a virtual method without using an abstract class also works though, maybe, hopefully haha
Yes I follow that. Custom inspector is given a list of skill base to choose from, hence the drop down. My experience with custom editor dynamically loading collections for use has been limited to prefabs and SO assets using the loadassets/resources method. Can the same be done for scripts selected by it’s base class inheritance ? Or whats the approach suppose to be with that?
And is drag and drop custom editor functionality supported in unity?
I also looked up Odin since you mentioned it. Does it handle abstract class lists ?
Thanks for the example. Never worked on attributes or types before, so the generic property drawer looks like deep water to me. For your note about custom drawers for collections, does it not draw custom collections at all or just that you can’t add elements via a custom drawer ?
Correct, they are designed for immutable data you want to store in an asset format.
You can do that with ScriptableObject.
Code
public abstract class Skill : ScriptableObject
{
public abstract void UseSkill();
}
using UnityEngine;
[CreateAssetMenu]
public class DamageSkill : Skill
{
public int amount;
public override void UseSkill()
{
Debug.Log($"Activated '{this.name}' doing '{amount}' damage.");
}
}
using UnityEngine;
public class SkillExample : MonoBehaviour
{
[SerializeReference] public Skill skill;
private void Start()
{
skill.UseSkill();
}
}
The issue with treating scriptable objects as normal OOP objects with methods and mutable state, is that the same scriptable object instance is shared by all the different Objects with serialized fields into which the scriptable object asset has been dragged.
So this would means that if one character used a skill, and it for example activated a cooldown, that cooldown would affect all characters, not just the one that used the skill.
It’s however possible to avoid this, by having each character create an instance of the skills they’re using during their initialization. This way changes made to their own skill instance won’t have any effect on the skills used by other characters.
This is exactly the same thing you do when you instantiate a prefab; you just use the asset as a blueprint for creating instances.
public class Character : MonoBehaviour
{
[SerializeField] private SkillList skillList;
// Create a unique clone of the SkillList just for this character
private void Awake() => skillList = skillList.Clone();
// Release the SkillList clone from memory once this character no longer needs it
private void OnDestroy() => skillList.Dispose();
}
[CreateAssetMenu]
public sealed class SkillList : ScriptableObject, IDisposable
{
[SerializeField] private Skill[] skills;
public SkillList Clone()
{
// clone the SkillList itself
var clone = Instantiate(this);
// also clone each of the Skill assets
clone.skills = skills.Select(Instantiate).ToArray();
return clone;
}
public void Dispose()
{
foreach(var skill in skills)
{
Destroy(skill);
}
Destroy(this);
}
}
public abstract class Skill : ScriptableObject
{
public abstract void Use(Character user);
}
Right, i see how this could work. But if a character’s skill list can change, i’d have to manage the associated SO asset seperately from managing the character. Rather than it just being serialized as part of the character. Seems like it solves for inspector exposure, but opens up the end of having to manage another asset. Which between the two, I’m leaning to just using a custom editor, and keeping the skills serialized under the character.
In the broader scheme of things, the idea was to manage 1 party SO asset, have it contain a list of characters, each of which have a list of skills,stats,attributes, and a prefab model. In a gameplay scene i would then attach deployed characters to an instance of their prefab and go from there. Seems to make the most sense, and maintainability wise.
Thanks, this was such a simple solution, which is the level I like to operate at. I was able to quickly test it out. It works in the inspector with no need for custom editor/property drawer. The main tiny drawback i see is that editing/updating fields in the script doesn’t get reflected in prior generated assets, but just changing/setting the fields in the asset itself works around that. Changes to the override method seems to reflect in the asset.
This is what I desired for editor use when making NPCs. Just need to check if I can get it to serialize within the character so the player characters’ skill list changes at runtime persist.
Sorry I’m not really sure what you’re asking here. I’ve already shown you how to serialise, select, and draw derived types in the inspector. What else do you need exactly? When you say ‘scripts’ do you mean monobehaviour components or scriptable objects? Or actual script files?
Otherwise you can’t have serialised data not part of an asset, nor can you have same data referenced across multiple assets, except for the case of referencing Unity objects. You can have data serialized to text files, and you can reference these as text assets, but then you need to de-serialise these for use.