Search Unity

Populating an Array with Scriptable Objects Directly Through Script

Discussion in 'Editor & General Support' started by Ziberian, Jul 26, 2021.

  1. Ziberian

    Ziberian

    Joined:
    Jan 9, 2018
    Posts:
    66
    Hello everyone,

    I researched this problem online before and the solutions I've found worked, but I just still have this itch that a better solution must exist. Here is my situation/problem:

    I have different weapon scriptable objects, and these weapons are to appear in the store at the end of a level. So an example gun scriptable object looks like: (All weapon scriptable objects are in a folder)

    upload_2021-7-26_14-25-39.png

    Right now what I do is: I have an array for my Shop UI game object, and I drag and populate the shop array with each weapon scriptable object, and it works fine:

    upload_2021-7-26_14-26-14.png

    But my problem is, as my game progresses I want to add a bunch of weapons, consumables, other purchasable items. After every addition I don't want to have to drag the new items, or try to remember if I had added them or not etc. Is there any way for me to just tell the game "Hey, look at this file and gather all the scriptable objects there into an array"? I have found about Resources.Load but a lot of people wrote that I shouldn't be using that because it is old and it is better to populate by hand?

    Another reason why I need this is, potentially I might even have a "make your own item" in my game, and allow players to design their own stuff and add them to the possible options in the shops.

    Thanks for reading, any help is appreciated ^_^
     
  2. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    38,745
    Complete hogwash. The use case you are speaking of is PRECISELY what Resources.Load<T>() is intended for, and precisely what I use it all the time for.

    Let's say you have a
    ShopItem
    that is a ScriptableObject, and you make a folder called
    AllMyShopItems/
    and place that folder under a folder named Resources:

    - use the CreateAssetMenu attribute to make your ScriptableObjects with one click
    - make them all in that folder
    - at runtime load them all with:

    Code (csharp):
    1. ShopItem[] AllItems = Resources.LoadAll<ShopItem>( "AllMyShopItems/");
    DONE.

    This requires a different approach. You would create runtime-transient ShopItems and have a user-facing editor to let them pre-make or tweak the available settings. You would then need to serialize this with your savegame data.

    When bringing it back in, remember this:

    When loading, you can never re-create a MonoBehaviour or ScriptableObject instance directly from JSON. The reason is they are hybrid C# and native engine objects, and when the JSON package calls
    new
    to make one, it cannot make the native engine portion of the object.

    Instead you must first create the MonoBehaviour using AddComponent<T>() on a GameObject instance, or use ScriptableObject.CreateInstance<T>() to make your SO, then use the appropriate JSON "populate object" call to fill in its public fields.
     
    bobisgod234 and april_4_short like this.
  3. Ziberian

    Ziberian

    Joined:
    Jan 9, 2018
    Posts:
    66
    Thank you very much for the in-depth reply, what exactly do you mean at the remember this part? Is that specifically for allowing players to design their own weapons, or just using the resources.load in general? I think you mean bringing back the player created weapons to be saved?
     
  4. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    38,745
    Correct. If you make a ShopItem (a ScriptableObject), you make them in your game, each one is a file on disk, you load them (as above), you use them.

    If you want to make an editor to let the user make them at runtime (remember, they won't have the Unity editor!) to make custom ShopItems, they would likely want to save them to run the next time. They gotta go somewhere, and they cannot be directly saved as such to be part of your game. That's where a normal savegame process comes in and you would need to recreate them by calling ScriptableObject.CreateInstance<T>() first, THEN overlaying the JSON into that created object.
     
  5. spiney199

    spiney199

    Joined:
    Feb 11, 2021
    Posts:
    7,930
    To specifically answer your question, yes you definitely can. I use the following extension method:

    Code (CSharp):
    1.  
    2.     using System.Linq;
    3.     using System.Collections;
    4.     using System.Collections.Generic;
    5.     using UnityEngine;
    6.     using UnityEditor;
    7.  
    8.     public static class ScriptableObjectUtilities
    9.     {
    10.  
    11.         public static List<T> FindAllScriptableObjectsOfType<T>(string filter, string folder = "Assets")
    12.             where T : ScriptableObject
    13.         {
    14.             return AssetDatabase.FindAssets(filter, new[] { folder })
    15.                 .Select(guid => AssetDatabase.LoadAssetAtPath<ScriptableObject>(AssetDatabase.GUIDToAssetPath(guid)))
    16.                 .Cast<T>().ToList();
    17.         }
    18.     }

    Bit of a long line of code there but it gets the job done. The filter you would use would be along the lines of 't:ShopItem' or whatever the class is for the ScriptableObject you're looking for.

    I'm gonna have to disagree with Kurt too and say there's good merit in pre-assigning these lists so you aren't reloading the same assets over and over. Especially if that lists starts to get inordinately large. Better off just pulling a 'here's one I prepared earlier' and have that set from the get go.

    I do this sort of pattern a lot, using editor scripts to look for the correct values and assign them ahead of time, rather than having to load them at run-time.

    One thing to note is that any assets (such as meshes, materials, etc) that are referenced in a ScriptableObject is loaded into memory when the SO is, which includes if said SO is references in one of these global lists. Generally addressables is a good way to manage this.
     
  6. Ziberian

    Ziberian

    Joined:
    Jan 9, 2018
    Posts:
    66
    To be honest I am not really following what is going on with the code you pasted there. Does that static method retrieve scriptable objects of a type, and in a specific folder? Because if that is the case that is pretty cool. I though they HAD to be in a folder called Resources
     
  7. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    38,745
    No, that's only for runtime ad-hoc loading. I inferred that you wanted this at runtime so you wouldn't have to keep shop item lists up to date and it would "just work" by virtue of an item being in a directory.

    Spiney's script above is for editor use only and with editor tooling you can happily find anything you need in your entire project so you can get at it and do stuff with it.
     
    Ziberian likes this.
  8. spiney199

    spiney199

    Joined:
    Feb 11, 2021
    Posts:
    7,930
    What Kurt says is correct, the script I shared is for Editor use only, which is what I assumed was the use case you were asking for.

    To break down the code a bit:
    • AssetDatabase.FindAssets is being used to return a list of strings containing GUID's for each asset found matching the filter in the chosen folder
    • The Linq Select() is then going through each GUID we found and using AssetDatabase.LoadAssetAtPath (using AssetDatabase.GUIDToAssetPath to make the path) to load a list of all the returned assets.
    • Because we've loaded assets of type ScriptableObject, and because Select returns an IEnumerable list, we then cast the list to the type we declared with <T>, and then convert it into a list, using both the .Cast<T>() and .ToList() Linq methods.
    So as a quick example of how you might use it:
    Code (CSharp):
    1. [SerializedField]
    2. private List<ShopItem> allShopItems = new List<ShopItem>();
    3.  
    4. [ContextMenu("Load All Shop Items")]
    5. private void LoadAllShopItems()
    6. {
    7.     allShopItems = ScriptableObjectUtilities.FindAllScriptableObjectsOfType<ShopItem>("t:ShopItem", "Assets/Your Folders Go Here");
    8. }
    The ContextMenu attribute adds a selection you can access by hitting the three little dots up near the top of the inspector window. Doing that will call the private method and load up your serialized list with all the assets it finds.
     
    Ziberian likes this.