Search Unity

  1. Megacity Metro Demo now available. Download now.
    Dismiss Notice
  2. Unity support for visionOS is now available. Learn more in our blog post.
    Dismiss Notice

[SOLVED] Unique Instances of ScriptableObject asset

Discussion in 'Scripting' started by Pzula, Nov 12, 2016.

  1. Pzula

    Pzula

    Joined:
    Jan 25, 2013
    Posts:
    7
    Hello all!

    A Brief Overview
    I have been trying to create a flexible system for creating items via ScriptableObjects. Here is my simple code for creating an item:

    Code (CSharp):
    1. [CreateAssetMenu(menuName="New Item")]
    2. public class Item : ScriptableObject {
    3.  
    4.     [SerializeField] private string m_name;
    5.     [SerializeField] private string m_description;
    6.  
    7. }
    As you can see, all this code does is create a new .asset file which will contain a name and description variables. I can set these variables via the inspector. So to create an Apple item my steps would look like this:

    • Assets > Create > New Item
    • A new .asset file appears in my assets
    • I name this Apple.asset
    • Set the name variable to Apple via the inspector
    • Set the description variable to Delicious! via the inspector
    I now have an Apple item asset. Creating a Cake item or a Recycled Boot is just as easy. Fantastic!

    The Problem
    From what I understand, this Apple.asset exists in my assets folder and can be referenced via references. That seems to be what ScriptableObjects are for: storing some data which can be referenced; Shared is the keyword here. The issue here is that items should be uniquely identifiable. If I have some Inventory database which holds two Apples, it is only holding two references to the same Apple asset. What if I wish to introduce unique attributes to these Items? e.g. Items now have a m_numberOfTimesUsed property which increments every time it is used. I cannot share this property value between items.

    As far as I understand you can't create an instance of a ScriptableObject asset. It would be great if I could create an instance of my Apple.asset but that just doesn't seem to be the way ScriptableObjects are intended to be used.

    Some Solutions
    • Create a UniqueItem class which holds a reference to the ScriptableObject Item asset:
    Code (CSharp):
    1. public class UniqueItem {
    2.  
    3.     public readonly Item item;
    4.  
    5.     public UniqueItem(Item item) {
    6.         this.item = item;
    7.     }
    8. }
    This would allow me to create new items by calling new UniqueItem(item) where item is a reference to a ScriptableObject Item asset. I can then compare two UniqueItem objects to see if they are the same instance or not (Going back to my Inventory database example, the two Apples would now be unique objects and have a reference to the Apple asset). The con of this approach is that I cannot easily create the m_numberOfTimesUsed property without adding this property to the UniqueItem class.
    • Forget about ScriptableObjects. This means I lose support for Unity's inbuilt serialization. I also lose the benefits of easy item creation with the editor. Every new item must be created via a new class and must be constructed via scripting. If I'm specifying the item's sprite, I'll need to set the sprite path explicitly. The creation process becomes difficult and not very user friendly.
    Am I missing something here, or just misunderstanding the use of a ScriptableObject? I seem to be wanting to use ScriptableObject assets as a 'blueprint' for item instantiation. As I say that it sounds just like I am describing a class and instantiating it via new.

    Any clarity on this situation is greatly appreciated.

    Thank you,

    -Pzula

    EDIT: Solved

    Thanks to LaneFox's answer, creating a unique instance of a .asset ScriptableObject is as easy as:
    Code (CSharp):
    1. Item newUniqueItem = Object.Instantiate(appleAsset);
    or a more generic implementation:

    Code (CSharp):
    1. MyScriptableObject newUniqueSO = Object.Instantiate(scriptableObjectAsset);
    where MyScriptableObject is a class deriving from ScriptableObject, and scriptableObjectAsset is a reference to the .asset file.
     
    Last edited: Nov 12, 2016
    Bigrouille and paternostrox like this.
  2. LaneFox

    LaneFox

    Joined:
    Jun 29, 2011
    Posts:
    7,462
    Why not use CreateInstance?

    SO's are a good approach for what you're doing. To unbind from the prefab just create instances.
     
  3. Pzula

    Pzula

    Joined:
    Jan 25, 2013
    Posts:
    7
    CreateInstance only seems to make an instance of a ScriptableObject class type which in my case would be an Item. I can't use CreateInstance to create a new instance of the Apple.asset which already has the variable values for name and description.

    Perhaps I'll have to go with my UniqueItem class approach. I can put the properties that are unique to each item instance in there (how many times an axe has chopped a tree, or how many bites of cake are left), and also have a reference to the .asset ScriptableObject. Makes sense as the .asset will only hold static information such as the item name, description, sprite graphic, etc; there is no need to make duplications of these values.
     
    Last edited: Nov 12, 2016
  4. LaneFox

    LaneFox

    Joined:
    Jun 29, 2011
    Posts:
    7,462
    Why do you need to create a new .asset? You need to track the unique item data in a class on a per-item basis and creating an instance of the original ScriptableObject allows you to do exactly that.
     
  5. Pzula

    Pzula

    Joined:
    Jan 25, 2013
    Posts:
    7
    I am not wanting to create a new .asset for every unique item in my game; I wish to create a ScriptableObject instance using the information from the Apple asset. I am seeing the .asset instance as the blueprint for a unique Item instance. There is only one Apple.asset which is an instance of the Item ScriptableObject and it lives in the asset folder. The Apple.asset correctly sets the name and description variables accordingly and this is done through the inspector.

    I do not know how to create an instance of this Apple asset. The CreateInstance function you linked takes the type of the ScriptableObject to create which will just give me a new blank Item ScriptableObject, rather than one with the values from the Apple asset.

    Forgive me if I am not understanding you correctly, or my understandings on ScriptableObjects are incorrect.
     
  6. LaneFox

    LaneFox

    Joined:
    Jun 29, 2011
    Posts:
    7,462
    Derp, Thats what I get for posting before coffee! You can just use Object.Instantiate(item) when you want an instance of the .asset.
    Code (csharp):
    1. Item NewUniqueItem = Object.Instantiate(AppleAsset)
    That gives you a unique instance of the Apple and you can adjust it all you want. The Item system I made looks like this:

    Code (csharp):
    1. public virtual int AddItem(string name, int amount = 1)
    2.         {
    3.             return AddItem(ItemUtility.FindItemByName(name), amount);
    4.         }
    This bounces to the other AddItem() method which handles all the inventory and ui stuff.

    Code (csharp):
    1.         public static ItemData FindItemByName(string title)
    2.         {
    3.             if (ItemDb == null) LoadItemDb();
    4.             ItemData value = ItemDb.Items.First(item => item.Title == title);
    5.             return value;
    6.         }
    Code (csharp):
    1.  
    2.     [CreateAssetMenu(fileName = "New Item Database", menuName = "Isobox/Item Database", order = 100)]
    3.     public class ItemDatabase : ScriptableObject
    4.     {
    5.         public List<ItemData> Items;
    6.     }
    This pulls from a list of ItemData scriptableobject .asset files which is maintained by a custom editor that i use to create items. If I wanted to have unique items then I have a separate method that uses the given unique runtime item instead of finding the base item from the database. That would be the case when picking up items and stuff.

    The point being that you don't have to do anything special when using ScriptableObjects, you can just create instances of them and they behave (basically) like normal classes do, allowing you to do pretty much whatever you want.
     
    Last edited: Nov 12, 2016
    Duffer123, paternostrox and Pzula like this.
  7. Pzula

    Pzula

    Joined:
    Jan 25, 2013
    Posts:
    7
    You have got to be joking! It is that simple. Thank you very much, this does exactly what I'm after. I must have been too terrified to try other means of ScriptableObject instantiation after Unity suggested I used ScriptableObject.CreateInstance. It's an intimidating engine at times. Thank you for sharing an insight into your Item system also; it is a very neat system you've got going there :). I imagine by having the ItemDatabase be a ScriptableObject you get the perks of the inspector, custom editors (as you mentioned), serialization, and the ease of swapping between databases. These ScriptableObjects really are magic!
     
    Duffer123, Pharaoh_ and LaneFox like this.
  8. LaneFox

    LaneFox

    Joined:
    Jun 29, 2011
    Posts:
    7,462
    No problem! Sorry for the initial confusion ;) SO's are pretty amazing, don't be afraid to ask more questions and experiment.
     
  9. Ziflin

    Ziflin

    Joined:
    Mar 12, 2013
    Posts:
    132
    @LaneFox Sorry to necro this post, but I was trying to nest some ScriptableObject-derived types that are referenced from a MonoBehavior (to get null/polymorphism support). This works fine with a custom editor to create new instances, but Unity does not duplicate the nested SO if you say duplicate owning GameObject in the scene.

    I was just curious if you knew of a way to have unity duplicate the nested SO as well? Maybe an attribute or flag I'm missing.
     
  10. Seaniboy2009

    Seaniboy2009

    Joined:
    Apr 23, 2019
    Posts:
    14
    Hi,

    sorry to reactive this old form, i am currently trying the same thing and am having issues. i have a medpack when its picked up it has a SO and this gets added to the inventory, once there you can click it and it heals you for the amount of usages it has, but when its fully used it destroys all other medpacs in the inventory.
     
  11. palex-nx

    palex-nx

    Joined:
    Jul 23, 2018
    Posts:
    1,748
    You really should make difference between Medpack prototype object asset and it's runtime instances. Threat original .asset file as descriptin and it's instances as described objects and that's it.
     
  12. Seaniboy2009

    Seaniboy2009

    Joined:
    Apr 23, 2019
    Posts:
    14
    Thanks palex-nx, i have solved the pickup and usage issue, but if i drop the item before its used it sets the usage counter back to original.

    when i drop the item i have my SO script create it again not sure this is the correct way,
     
  13. palex-nx

    palex-nx

    Joined:
    Jul 23, 2018
    Posts:
    1,748
    No, it is not. Whenever item first time dropped into scene, it should persist. When player drops the item, it already have scriptable object instance, so why creating another one? Use existing.
     
  14. codexin

    codexin

    Joined:
    Apr 4, 2018
    Posts:
    4
    You can varies and randomize each time a scriptableobject is using inside OnEnable pretty simple.
     
  15. s_marcell

    s_marcell

    Joined:
    Mar 22, 2018
    Posts:
    19
    What would be the best approach to safety check whether I'm trying to interact with the .asset itself?
     
  16. spiney199

    spiney199

    Joined:
    Feb 11, 2021
    Posts:
    7,606
    Better answer would be to just never modify scriptable objects, and never instance them.

    If you need mutable values, use a plain C# object that wraps around your scriptable object instance and acts as the intermediary. If you want them to be treated the same, you can outline an interface that expresses all relevant functions.

    Basically, the asset is a blueprint that never changes. The plain C# object represents an actual 'live instance' in your game that can change.

    You can make the wrapper have an indirect reference to the actual asset too, such as having a locator that looks up the item from a database. Effectively so long as you don't have any actual serialised Unity object references, your instances can be serialisable without the need to make serialised surrogates.
     
    neonblitzer and Nad_B like this.