Automatically add ScriptableObject asset to a list on game start

Is it possible to add Scriptable Objects that are created as custom menu assets in Unity to a static list on game start?

Something like this:

[CreateAssetMenu(menuName = "Custom/SomeData")]
public class SomeData:ScriptableObject
{
    public static List<SomeData> _someDataList = new List<SomeData>();

    private void OnEnable()
    {
        _someDataList.Add(this);
    }
}

This isn’t working, but I would need something to prevent me from having to drag and drop every single custom menu asset to some list inside the editor and then find a way to retrieve the data I need in a particular situation.

So my question is: is there a way to automatically add a scriptable object/custom menu asset to a list on creation? Or do you see what misapprehension I’m under and how I could do this in a different way?

Any help would be highly appreciated! Thanks! :slight_smile:

Hi, as far as my knowledge for scriptableobjects go, they just store data; so any monobehaviour function as onEnable and such does not work that well on them .

Coincidentally I also had a similar issue, so if you wanna you can check a thread I opened a few days ago.

I ended up using a second scriptableobject holding a string array with the list of items I wanted to have in my inventory every time I load a save file and then I just put it all together in some script I run at the start of my game.

1 Like

It seems that functions like OnEnable are just called at a different time for SOs than for MBs. For menu assets, it looks like OnEnable is aleady called in the inspector, which kind of makes sense, but this means, that there is no function that is called when the game starts, which also makes sense, because they are not necessarily in the build.

What I don’t really understand is, when is Awake called? In the reference it says:

Awake is called as the ScriptableObject script starts. This happens as the game is launched and is similar to MonoBehavior.Awake.

But I suppose this is not true for custom menu assets and only if you create an instance of a SO at runtime? Again, it kinda makes sense, but it doesn’t make me happy…

Thanks for your answer! I will check out your thread, but I think I will have to rethink my whole approach and maybe I just give up on scriptable objects as data containers, because they are driving me nuts xD

I’d second using a separate ScriptableObject “container”. You don’t need OnEnable or Awake if you avoid the CreateAssetMenu attribute and write your own static function to populate the list. See AssetMenu.cs

  • The static functions at the top illustrator how to use the MenuItem attribute
  • The CreateAsset function contains the code you’ll need to create new ScriptableObject asset files
  • You then just add the new asset to the list on creation and reference the “container” at runtime
1 Like

If you don’t want to have to reference an object everywhere for access to the list you could setup a singleton class that when first called finds the list object & then you can call that anywhere e.g.

public class SingletonClass
{
   SOList m_loadedList;

   private SingletonClass()
   {

   }

   static SingletonClassm_instance;
   
   static SingletonClass Get()
   {
      if(m_instance == null)
         m_instance = new SingletonClass();

      return m_instance;
   }

   public static List<SOData> GetList()
   {
      SingletonClass myClass = Get();

       if(myClass.m_loadedList == null)
          //Find the SO

      return myClass.m_loadedList.list;
   }
}

Then you can just call SingletonClass.GetList() anywhere for the data you want.

1 Like

@lordconstant I would go with this solution, but this way I’d still have to add all my SO assets to a list by hand, which I’m trying to avoid.

@grizzly I find your suggestion intriguing, probably because it is a little bit out of my skillset – I really suck at editor programming ;D Which is too bad and maybe this is a good place to start. I have some trouble understanding the last point on your list, “You then just add the new asset to the list on creation and reference the “container” at runtime”. How would I instantiate the container holding the list of the created asset files?

Just to make sure I get it right: You’re saying I should use the MenuItem attribute to call a custom method to create scriptable object assets that are then added to a (static?) list of a container class/object, which I can reference during runtime?

My solution was to work alongside grizzly’s, by container they’re referring to a scriptable object that just holds the others as a list.

1 Like

Oh I’m sorry, I misunderstood! Probably because I don’t fully understand grissly’s answer yet. Could you tell me (in outline) how to populate the list of this container SO for runtime? This is the main part I have trouble understanding.

Awake is called when an instance is created from CreateInstance.

For adding SO’s to a list, I just put them in a folder and use Resources.LoadAll.

public class Database : MonoBehavior
{
    public static List<SomeData> _someDataList = new List<SomeData>();

    void Awake()
    {
        foreach (SomeData data in Resources.LoadAll<SomeData>("FolderName"))
        {
             someDataList.Add(data);
        }
    }
}

Or I guess you could just use .ToList() on the LoadAll if you put ‘using System.Linq;’

_someDataList = Resources.LoadAll<SomeData>("FolderName").ToList();
1 Like

Not necessary. You may create a script attached to a button in some sort of hidden debug menu to allow you to first copy the data of your inventory list/item list/Data list to the second scriptableobject container with the string array/other method you choose. That way you can update this second scriptableobject with the container list anytime you need just running your script in the debug menu. At least, it’s the solution I came up for myself.

Good luck!

1 Like

Yes, precisely.

Sure, here’s an example…

  • Create the following two class files

  • Create an “Assets/Resources” folder containing a ScriptableObject asset file for your “Database”

  • Run this from the “Custom/SomeData” menu

// Belongs in its own file ("Database.cs")
public class Database : ScriptableObject
{
    public List<Data> SomeData;

    private static int m_Index = 0;

    [MenuItem("Custom/SomeData")]
    private static void CreateAndAddSomeData ()
    {
        // Path to store the data
        var path = $"Assets/Resources/Data_{m_Index++}.asset";

        // Create the data asset
        var asset = ScriptableObject.CreateInstance<Data>();
        AssetDatabase.CreateAsset(asset, path);
        AssetDatabase.SaveAssets();

        // Load the database asset (from "Resources/Database.asset")
        var database = Resources.Load<Database>("Database");
        if (database.SomeData == null)
            database.SomeData = new List<Data>();

        // Add the data
        database.SomeData.Add(asset);

        // Tell Unity to save the changes
        EditorUtility.SetDirty(database);
    }
}

// Belongs in its own file ("Data.cs")
public class Data : ScriptableObject
{
}

The benefit of this is that the database will persist so you avoid additional overhead at runtime.

1 Like

Thank you all very much! That is an elegant solution grizzly and I just might make an exception on my “rule” not to use the Resources folder :wink: I think I have all I need, but if someone still has ideas or ways to do this without the Resources folder, I’m interested in your additions.

You’re welcome, but no need to make exceptions as it’ll work just as well outside of the Resources folder.

Change line 20 in the code above with this, and set your paths accordingly;

var database = AssetDatabase.LoadAssetAtPath<Database>("Assets/Database.asset");

You can then create a public Database variable on a script in your scene and drag and drop the asset there to access it at runtime.

1 Like

Very cool! I thank you very much grizzly for your continuous help and detailed explanations! I could still go on with questions, but I figure it is enough already for one thread and I will read into editor programming before tackling the next issues. Thank you!

1 Like

I handled this in my Datasacks class because I wanted a globally-accessible asset to find and load all instances of Datasacks, which are ScriptableObjects.

Notably the reason you cannot use OnEnable() is because ScriptableObjects don’t have lifecycles that mirror the MonoBehavior lifecycle.

To handle it, I wrote an editor script that finds all Datasack assets in the project, then adds them to a master DatasackCollection object and creates an instance of that in your project.

Datasacks is presently hosted at these locations:

https://bitbucket.org/kurtdekker/datasacks

The relevant file that does most of this is here:

Look in the GenerateCode() method there: it produces some source code AND it creates the DatasackCollection asset where I can load it with Resources.Load at runtime.

2 Likes

Hi All! I think I’ve found a solution which MIGHT be a little better since it doesn’t use the Resources.Load + not even static! I haven’t seen it anywhere else so I’m curious on your opinions!

I’ve created a scriptable object container which doesn’t load on Start, but even BEFORE that in the inspector! This way it can even be moved out of the resources folder.

Solution:
Here you will see a simple recipes database and a recipe scriptable object.

Recipe ScriptableObject:

using System.Collections.Generic;
using UnityEngine;

[CreateAssetMenu(fileName ="New Recipe", menuName = "Recipe")]
public class Recipe : ScriptableObject
{
    public string itemId;
    public string failedId;
    public List<string> ingredients;
    public string toolId;
}

RecipeDatabase ScriptableObject:

using System.Collections.Generic;
using UnityEngine;

[CreateAssetMenu(fileName = "New Database", menuName = "Databases/Recipes")]
public class RecipeDatabase : ScriptableObject
{
    public List<Recipe> recipes;
}

The issue with this is you need to manually drag the recipes into the database. In the script below I’ve created an inspector script which will add a simple checkbox to add your recipe to add it to the database! When opening the scriptable object it will also read the database and see if it’s there. If so it sets the default value to toggled. Got the idea from addressables but that addressables ended up not being what I wanted.

RecipeDatabaseEditor Script:

#if (UNITY_EDITOR)
using UnityEngine;
using UnityEditor;
[CustomEditor(typeof(Recipe))]
public class RecipeEditor : Editor
{
    RecipeDatabase db;
    private bool isInitiated = false;
    private bool currentToggleValue;
    private bool lastToggleValue;

    public override void OnInspectorGUI()
    {
        var recipe = (Recipe)target;

        base.OnInspectorGUI();

        if (!isInitiated)
        {
            db = (RecipeDatabase)AssetDatabase.LoadAssetAtPath("Assets/RecipeDatabase/Recipes.asset", typeof(RecipeDatabase));
            currentToggleValue = db.recipes.Contains(recipe);
            lastToggleValue = currentToggleValue;
            isInitiated = true;
        }

        currentToggleValue = GUILayout.Toggle(currentToggleValue, "Added to Database", GUILayout.Height(20));
        if (lastToggleValue != currentToggleValue)
        {
            if (currentToggleValue)
            {
                db.recipes.Add(recipe);
                EditorUtility.SetDirty(db);
            }
            else
            {
                db.recipes.Remove(recipe);
                EditorUtility.SetDirty(db);
            }

            lastToggleValue = currentToggleValue;
        }
    }
}
#endif

Let me know what you think!