Search Unity

  1. Welcome to the Unity Forums! Please take the time to read our Code of Conduct to familiarize yourself with the forum rules and how to post constructively.
  2. We have updated the language to the Editor Terms based on feedback from our employees and community. Learn more.
    Dismiss Notice

Question inheritance from Scriptable Object + Generic

Discussion in 'Scripting' started by Grzp, Oct 6, 2022.

  1. Grzp

    Grzp

    Joined:
    Jan 25, 2018
    Posts:
    31
    Hi guys,
    I have a small problem. So I have a scriptable object class:
    Code (CSharp):
    1. public class Item : ScriptableObject
    2. {
    3.     [SerializeField] private string m_name;
    4.     public string Name => m_name;
    5.  
    6.     [SerializeField] private Sprite m_spriteItem;
    7.     public Sprite SpriteItem => m_spriteItem;
    8. }
    A few class inherit from it:
    Code (CSharp):
    1. public class Weapon : Item
    2. {
    3.     [SerializeField] private int m_damage;
    4.     public int Damage => m_damage;
    5. }
    6.  
    Code (CSharp):
    1. public class Shield : Item
    2. {
    3. }
    4.  
    And I have a class where I create this scriptable object:
    Code (CSharp):
    1. public abstract class Items<T> : ScriptableObject where T : Item
    2. {
    3.     protected const string ASSET_EXTENSION = ".asset";
    4.     [SerializeField, FolderPath(RequireExistingPath = true), Required] protected string m_scriptableObjectPath;
    5.  
    6.     [SerializeField] private List<T> m_items = new List<T>();
    7.     public List<T> ListOfItems => m_items;
    8.  
    9.     [SerializeField, FolderPath(RequireExistingPath = true), Required] protected string m_scriptableObjectPathForNewItem;
    10.  
    11.     [field: SerializeField, ValueDropdown("FindItemClass")] public T NewItem { get; private set; }
    12.  
    13.     [SerializeField] private string m_newItemName;
    14.     public string NewItemName => m_newItemName;
    15.  
    16.     [SerializeField] private Sprite m_newItemSprite;
    17.     public Sprite NewSpriteItem => m_newItemSprite;
    18.  
    19.  
    20.     [Button("Create item")]
    21.     private void CreateItem()
    22.     {
    23.         if (m_newItemName != string.Empty && m_scriptableObjectPath != string.Empty)
    24.         {
    25.             AssetDatabase.CreateAsset(NewItem, m_scriptableObjectPathForNewItem + "/" + m_newItemName + ASSET_EXTENSION);
    26.             AssetDatabase.SaveAssets();
    27.         }
    28.     }
    29. }
    And then I want to inherit from Items class:
    Code (CSharp):
    1. public class Weapons : Items<Weapon>
    2. {
    3.  
    4. }
    5.  
    And next I have class:
    Code (CSharp):
    1. public class CreateItems<T> : CreateDataBaseClass where T : Items<Item>
    2. {
    3.     [field: SerializeField] public T Items { get; private set; }
    4.  
    5.     public CreateItems()
    6.     {
    7.         Items = ScriptableObject.CreateInstance<T>();
    8.     }
    9.  
    10.     [Button("Create list with items"), PropertyOrder(0)]
    11.     protected override void CreateData()
    12.     {
    13.         if (m_scriptableObjectName != string.Empty && m_scriptableObjectPath != string.Empty)
    14.         {
    15.             AssetDatabase.CreateAsset(Items, m_scriptableObjectPath + "/" + m_scriptableObjectName + ASSET_EXTENSION);
    16.             AssetDatabase.SaveAssets();
    17.         }
    18.     }
    19. }
    And on the and I want to create object from CreateItems<> like this:
    Code (CSharp):
    1. public class PlayerDataWindow : OdinMenuEditorWindow
    2. {
    3.     [MenuItem("Windows/ Player data window")]
    4.     private static void OpenWindow()
    5.     {
    6.         GetWindow<PlayerDataWindow>().Show();
    7.     }
    8.  
    9.     private CreatePlayerData m_createPlayerMovmentData;
    10.  
    11.     private CreateItems<Weapons> m_createWeaponItems;
    12.     private CreateItems<Shields> m_createShieldsItems;
    13.  
    14.     protected override OdinMenuTree BuildMenuTree()
    15.     {
    16.         var tree = new OdinMenuTree();
    17.  
    18.         m_createPlayerMovmentData = new CreatePlayerData();
    19.  
    20.         m_createWeaponItems = new CreateItems<Weapons>();
    21.         m_createShieldsItems = new CreateItems<Shields>();
    22.  
    23.  
    24.         tree.Add("Create New Player Data", m_createPlayerMovmentData);
    25.         tree.Add("Create new weapons List", m_createWeaponItems);
    26.         tree.Add("Create new shields list", m_createShieldsItems);
    27.  
    28.  
    29.         tree.AddAllAssetsAtPath("Palyer data", "Assets/ScriptableObjects/PlayerData/", typeof(PlayerData));
    30.         tree.AddAllAssetsAtPath("Items", "Assets/ScriptableObjects/Items/Weapons/", typeof(Items<Weapon>));
    31.         tree.AddAllAssetsAtPath("Items", "Assets/ScriptableObjects/Items/Weapons/", typeof(Items<Shield>));
    32.  
    33.         return tree;
    34.     }
    35.  
    36.     protected override void OnDestroy()
    37.     {
    38.         base.OnDestroy();
    39.  
    40.         if (m_createPlayerMovmentData != null)
    41.         {
    42.             DestroyImmediate(m_createPlayerMovmentData.PlayerData);
    43.         }
    44.     }
    45.     protected override void OnBeginDrawEditors()
    46.     {
    47.         OdinMenuTreeSelection odinMenuSelectied = this.MenuTree.Selection;
    48.  
    49.         SirenixEditorGUI.BeginHorizontalToolbar();
    50.         {
    51.             GUILayout.FlexibleSpace();
    52.  
    53.             if (SirenixEditorGUI.ToolbarButton("Delete current"))
    54.             {
    55.                 PlayerData asset = odinMenuSelectied.SelectedValue as PlayerData;
    56.  
    57.                 string pathToAsset = AssetDatabase.GetAssetPath(asset);
    58.                 AssetDatabase.DeleteAsset(pathToAsset);
    59.                 AssetDatabase.SaveAssets();
    60.             }
    61.         }
    62.         SirenixEditorGUI.EndHorizontalToolbar();
    63.     }
    64. }
    65.  
    And I have an error from line:
    upload_2022-10-6_20-17-35.png
    And the message is: upload_2022-10-6_20-18-16.png
    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:
    Code (CSharp):
    1. public class Weapons : Items<Item>
    2. {
    3.  
    4. }
    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.
     
    Last edited: Oct 6, 2022
  2. PraetorBlue

    PraetorBlue

    Joined:
    Dec 13, 2012
    Posts:
    7,735
    Shouldn't it be:
    public class CreateItems<T> : CreateDataBaseClass where T : Item

    instead of
    public class CreateItems<T> : CreateDataBaseClass where T : Items<Item>

    ?
     
  3. Grzp

    Grzp

    Joined:
    Jan 25, 2018
    Posts:
    31
    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.
     
  4. Bunny83

    Bunny83

    Joined:
    Oct 18, 2010
    Posts:
    3,571
    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 :)
     
    Grzp and Kurt-Dekker like this.
  5. spiney199

    spiney199

    Joined:
    Feb 11, 2021
    Posts:
    6,003
    I think OP is better off making these databases just be
    List<Item>
    and omit the generic parameters.
     
    Grzp, PraetorBlue and Bunny83 like this.
  6. SisusCo

    SisusCo

    Joined:
    Jan 29, 2019
    Posts:
    1,135
    You can add two generic types to the CreateItems class
    Code (CSharp):
    1. public class CreateItems<TItems, TItem> : CreateDataBaseClass
    2.                                           where TItems : Items<TItem>
    3.                                           where TItem : Item
    4. {
    5.     [field: SerializeField] public TItems Items { get; private set; }
    6.  
    7.     public CreateItems()
    8.     {
    9.         Items = ScriptableObject.CreateInstance<TItems>();
    10.     }
    With this change you can get your code to compile:
    Code (CSharp):
    1. private CreateItems<Weapons, Weapon> m_createWeaponItems;
    2. private CreateItems<Shields, Shield> m_createShieldsItems;
     
    Grzp and Bunny83 like this.
  7. Grzp

    Grzp

    Joined:
    Jan 25, 2018
    Posts:
    31
    Thanks for replies, I will read it and test these solutions if I find a free time for it :)