inheritance from Scriptable Object + Generic

Hi guys,
I have a small problem. So I have a scriptable object class:

public class Item : ScriptableObject
{
    [SerializeField] private string m_name;
    public string Name => m_name;

    [SerializeField] private Sprite m_spriteItem;
    public Sprite SpriteItem => m_spriteItem;
}

A few class inherit from it:

public class Weapon : Item
{
    [SerializeField] private int m_damage;
    public int Damage => m_damage;
}
public class Shield : Item
{
}

And I have a class where I create this scriptable object:

public abstract class Items<T> : ScriptableObject where T : Item
{
    protected const string ASSET_EXTENSION = ".asset";
    [SerializeField, FolderPath(RequireExistingPath = true), Required] protected string m_scriptableObjectPath;

    [SerializeField] private List<T> m_items = new List<T>();
    public List<T> ListOfItems => m_items;

    [SerializeField, FolderPath(RequireExistingPath = true), Required] protected string m_scriptableObjectPathForNewItem;

    [field: SerializeField, ValueDropdown("FindItemClass")] public T NewItem { get; private set; }

    [SerializeField] private string m_newItemName;
    public string NewItemName => m_newItemName;

    [SerializeField] private Sprite m_newItemSprite;
    public Sprite NewSpriteItem => m_newItemSprite;


    [Button("Create item")]
    private void CreateItem()
    {
        if (m_newItemName != string.Empty && m_scriptableObjectPath != string.Empty)
        {
            AssetDatabase.CreateAsset(NewItem, m_scriptableObjectPathForNewItem + "/" + m_newItemName + ASSET_EXTENSION);
            AssetDatabase.SaveAssets();
        }
    }
}

And then I want to inherit from Items class:

public class Weapons : Items<Weapon>
{

}

And next I have class:

public class CreateItems<T> : CreateDataBaseClass where T : Items<Item>
{
    [field: SerializeField] public T Items { get; private set; }

    public CreateItems()
    {
        Items = ScriptableObject.CreateInstance<T>();
    }

    [Button("Create list with items"), PropertyOrder(0)]
    protected override void CreateData()
    {
        if (m_scriptableObjectName != string.Empty && m_scriptableObjectPath != string.Empty)
        {
            AssetDatabase.CreateAsset(Items, m_scriptableObjectPath + "/" + m_scriptableObjectName + ASSET_EXTENSION);
            AssetDatabase.SaveAssets();
        }
    }
}

And on the and I want to create object from CreateItems<> like this:

public class PlayerDataWindow : OdinMenuEditorWindow
{
    [MenuItem("Windows/ Player data window")]
    private static void OpenWindow()
    {
        GetWindow<PlayerDataWindow>().Show();
    }

    private CreatePlayerData m_createPlayerMovmentData;

    private CreateItems<Weapons> m_createWeaponItems;
    private CreateItems<Shields> m_createShieldsItems;

    protected override OdinMenuTree BuildMenuTree()
    {
        var tree = new OdinMenuTree();

        m_createPlayerMovmentData = new CreatePlayerData();

        m_createWeaponItems = new CreateItems<Weapons>();
        m_createShieldsItems = new CreateItems<Shields>();


        tree.Add("Create New Player Data", m_createPlayerMovmentData);
        tree.Add("Create new weapons List", m_createWeaponItems);
        tree.Add("Create new shields list", m_createShieldsItems);


        tree.AddAllAssetsAtPath("Palyer data", "Assets/ScriptableObjects/PlayerData/", typeof(PlayerData));
        tree.AddAllAssetsAtPath("Items", "Assets/ScriptableObjects/Items/Weapons/", typeof(Items<Weapon>));
        tree.AddAllAssetsAtPath("Items", "Assets/ScriptableObjects/Items/Weapons/", typeof(Items<Shield>));

        return tree;
    }

    protected override void OnDestroy()
    {
        base.OnDestroy();

        if (m_createPlayerMovmentData != null)
        {
            DestroyImmediate(m_createPlayerMovmentData.PlayerData);
        }
    }
    protected override void OnBeginDrawEditors()
    {
        OdinMenuTreeSelection odinMenuSelectied = this.MenuTree.Selection;

        SirenixEditorGUI.BeginHorizontalToolbar();
        {
            GUILayout.FlexibleSpace();

            if (SirenixEditorGUI.ToolbarButton("Delete current"))
            {
                PlayerData asset = odinMenuSelectied.SelectedValue as PlayerData;

                string pathToAsset = AssetDatabase.GetAssetPath(asset);
                AssetDatabase.DeleteAsset(pathToAsset);
                AssetDatabase.SaveAssets();
            }
        }
        SirenixEditorGUI.EndHorizontalToolbar();
    }
}

And I have an error from line:


And the message is:
The question is, is there a posibility to create class like I do in PlayerDataWindow class? And what I should change to fix this problem?

Also when I inherit in Weapons class like this:

public class Weapons : Items<Item>
{

}

I don’t have an error in editor, but it dosen’t work as I want.

If anybody prefer normal repository there is a link: pgproject/FPSGame (github.com)
Branch: PlayerEquipment

I will be so greatufull for help.

Shouldn’t it be:
public class CreateItems<T> : CreateDataBaseClass where T : Item
instead of
public class CreateItems<T> : CreateDataBaseClass where T : Items<Item>
?

But then I will skip creating instance of class Items, I don’t want do that. My goal is creating Scriptable Object from Items class (like: Shields, Weapons ect) and in class Items creating another Scriptable Object from Item, like Weapon, Shield ect.

What you try to do is not possible at all. I’ve explained that a couple of times in the past but most people have the wrong idea about generics. Generics is in some sense the opposite of inheritance. Class inheritance is about inheriting data but allow replacement of code / functionality. So you generally have an “is” relationship to the base class. So a derived class is literally the same as a base class and can be used as such implicitly.

Generics do the opposite. They are about using the same code / functionality but on different unconnected types. So you have fix functionality and you can exchange the type that code works on. That’s why generics have quite a few constraints what you can actually do with them as the code has to work with all possible variants that you allow in the generic arguments.

Since inheritance and generics are essentially opposing ideas you run into several issues when they clash together. There are two concepts which partially can solve those issues, but only in specific cases. Namely: covariance and contravariance. Though what’s important is: only one of those two things can apply to a generic class, never both. Those two concepts restrict the direction of the data flow of the generic type. Either data flows into the class or out of the class. Since you use your generic argument T in a List (which allows read and write) it’s impossible to get co- / contravariance.

Note that concrete classes of a generic class (so when a type is bound to the generic argument) are completely seperate and incompatible types. A List<Component> and a List<MonoBehaviour> are completely incompatible, even though MonoBehaviour is derived from Component.

The only cases where covariance or contravariance may apply are generic types where the generic argument has the modifier “out” or “in”. This is the case for System.Action<in T> and System.Func<out T> or IEnumerable<out T>. In those cases you get some assignment compatibility because the dataflow is ensured. “In” parameters can only be used inside the class / type for method arguments. So only in places where data flows into the class. “out” parameters can only be used as return values of methods or readonly properties.

Covariance is about the “out” constraint, contravariance is about the “in” constraint. So the assignment of an IEnumerable<GameObject> to an IEnumerable<object> variable is possible thanks to covariance since we only have outflowing data and that data is casted to a base class. The other way round is not possible. You can not assign an enumerable that returns the type “object” to a variable of an enumerable with a more specific type.

Likewise an assignment of a System.Action<GameObject> to a System.Action<object> is possible thanks to contravariance. Here the data is only flowing into the type. So a type expecting an object can also accept a more specific type since the more specific type is derived from the less specific type and therefore compatible.

As I said, since a List support both, read and write operation, no assignment compatibility is possible at all.

Finally: Note that native arrays play a special role in the while co- / contravariance topic. The runtime actually supports covariance for arrays, even though this allows illegal assignments. Though since array assignments are always type checked at runtime, trying to assign an illegal value would throw a runtime exception. Contravariance is not supported for arrays since reading array elements are not type checked.

So having a GameObject[ ] myGOs we can actually assign this array to a object[ ] objs = myGOs;. However trying to set a value of objs to an illegal value like objs[0] = 42; which is perfectly fine for the compiler but would through a runtime exception since you can not store an integer value in a GameObject array. This exception has been made for certain language features (especially reflection) which would otherwise be difficult to realise.

So to sum up: What you try to do is simply not possible, at all :slight_smile:

2 Likes

I think OP is better off making these databases just be List<Item> and omit the generic parameters.

3 Likes

You can add two generic types to the CreateItems class

public class CreateItems<TItems, TItem> : CreateDataBaseClass
                                          where TItems : Items<TItem>
                                          where TItem : Item
{
    [field: SerializeField] public TItems Items { get; private set; }

    public CreateItems()
    {
        Items = ScriptableObject.CreateInstance<TItems>();
    }

With this change you can get your code to compile:

private CreateItems<Weapons, Weapon> m_createWeaponItems;
private CreateItems<Shields, Shield> m_createShieldsItems;
2 Likes

Thanks for replies, I will read it and test these solutions if I find a free time for it :slight_smile: