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. Dismiss Notice

Load ScriptableObject as Singleton from Resources folder (tuto & questions)

Discussion in 'Scripting' started by DaCookie, Aug 29, 2017.

  1. DaCookie

    DaCookie

    Joined:
    Nov 4, 2014
    Posts:
    44
    Hi everybody.
    This question has been asked many times in many forums and Unity answers. Anyway, I have a solution to suggest for this "problem"... and a question about a strange behaviour in the editor. Let me explain.

    I'll talk about ScriptableObject singletons, a super cool pattern to avoid prefabs in the scene just to store resources to spawn or game settings, and so avoid Game Designers forgot to put them in scene or put prefabs that we don't want in scene (I love game designers, please don't be mad at me :p).

    I learned many tips in this video about right uses and best practices of ScriptableObjects. But I would like to focus on this particular sequence, about the "Reload-Proof Singleton".

    Problem :

    To illustrate my captivating story, make a new Unity project, and create the two following scripts. DemoConfig implements the Reload-proof singleton pattern, and GUILogger is just to get feedback of the problem.

    Code (CSharp):
    1. using UnityEngine;
    2. [CreateAssetMenu(fileName = "Demo Config", menuName = "Demo Config", order = 750)]
    3. public class DemoConfig : ScriptableObject
    4. {
    5.     private static DemoConfig s_Instance = null;
    6.  
    7.     private void OnEnable()
    8.     {
    9.         Debug.Log("DemoConfig -> OnEnable()");
    10.     }
    11.  
    12.     public static DemoConfig GetInstance()
    13.     {
    14.         if (!s_Instance)
    15.         {
    16.             DemoConfig[] all = Resources.FindObjectsOfTypeAll<DemoConfig>();
    17.             s_Instance = (all.Length > 0) ? all[0] : null;
    18.         }
    19.  
    20.         if (!s_Instance)
    21.         {
    22.             s_Instance = CreateInstance<DemoConfig>();
    23.             s_Instance.name = "Default";
    24.         }
    25.  
    26.         return s_Instance;
    27.     }
    28. }
    Code (CSharp):
    1. using UnityEngine;
    2. public class GUILog : MonoBehaviour
    3. {
    4.     private void OnGUI()
    5.     {
    6.         DemoConfig config = DemoConfig.GetInstance();
    7.         GUILayout.Box("Config : " + config.name);
    8.     }
    9. }
    There's some few steps left :
    • Make a "Resources" folder in yout project Assets
    • Create a "Demo Config" asset in it (Assets > Create > Demo Config)
    • Attach the GUILogger to the default "Main Camera" (or anything else in the scene, of course)
    • Play the game
    You should see a little GUI box in the top-left corner of the Game View with the message "Config : Demo Config". Great, GUILog can access to the DemoConfig asset in project folder without referencing it in its properties or using a path to that resource.

    But it's not that easy... Save your scene, close Unity and reopen your project. Don't click anywhere but on the Play button... So, what's in the GUI Box now ? Are you still smiling ? In fact, the Reload-proof singleton pattern is not designed for this.

    In forums, I saw that many people thought that this pattern is to make ScriptableObject singletons in their project resources. But, it's not really the objective : this pattern is to make Reload-proof singletons. It means that the object has to be loaded once, to be found in resources... and that's why you see the "default" DemoConfig asset name.

    In the video, Richard Fine explains that the problem is about serialization processes when you reload your project with statics variables initialized at runtime. And if you're a regular consumer of singletons, you must have experienced null ref errors at hot reload.
    The Reload-proof singleton patterns is an answer to that.

    Now you could tell me "but why the fuuf did it work the first time ???". A really good question... It worked because you loaded the asset. Go to your Unity project, just click on the Demo Config asset. You'll see it in the inspector... I mean, the inspector will load it. Then, run the game...

    ... Yeah, exactly. Note that if you build the game and start it, you'll see "Default" in the GUI box.

    It means that the method Resources.FindObjectsOfTypeAll<T>() works only for loaded resources, as explained in documentation :
    And resources in Resources folder are obviously not loaded "by default".

    Solution :

    So, what can we do ? Use Resources.Load() ? Nope, we still need to write a path, and change it when the asset is moved... but if I want to use the same script in another project, you'll need to change that path another time.
    We could use a prefab that store the asset and put it in the scene... STOP, DON'T DO THAT ANYMORE ! You have so many chances to get issues and time wasting to solve them with this kind of patterns... Richard Fine talks about it too in the video.
    Someone told me that put DemoConfig script before "Default time" in "Script Execution Order" will solve the problem... It doesn't at all : this will not load the resource anyway...

    The solution is, in fact, really simple :
    • Go to Edit > Project Settings > Player
    • Open "Other settings" tab
    • Add an entry in the "Preloaded Assets" array and place your Demo Config asset in it
    You can now see that it works in the Editor AND in the built game... That's a success, you now have an asset where you can store the objects to spawn, your game settings without pollute your scene or load assets or weird prefabs manually.

    Bug or feature ?

    But it still remains a problem. And I don't really know if this is a bug or not in the Unity Editor.
    Close the editor and reopen the project with the Demo Config asset in the "Preloaded Assets" array. If you run the game in the editor... There's still no Demo Config asset loaded (we see "Default" in GUI box).
    Is that normal ? You can easily notice that if you build and run the game, the resource will be loaded successfully.

    My questions are : is it an expected behaviour that "Preloaded Assets" is not used in the editor ? If yes, how to preload assets in the editor ?

    For now, my solution is to rewrite the DemoConfig.GetInstance() method :

    Code (CSharp):
    1. public static DemoConfig GetInstance()
    2.     {
    3.         if (!s_Instance)
    4.         {
    5.             DemoConfig[] all = Resources.FindObjectsOfTypeAll<DemoConfig>();
    6.             s_Instance = (all.Length > 0) ? all[0] : null;
    7.         }
    8.  
    9.         #if UNITY_EDITOR
    10.         if(!s_Instance)
    11.         {
    12.             string[] configsGUIDs = UnityEditor.AssetDatabase.FindAssets("t:" + typeof(DemoConfig).Name);
    13.             if (configsGUIDs.Length > 0)
    14.             {
    15.                 s_Instance = Resources.Load<DemoConfig>(UnityEditor.AssetDatabase.GUIDToAssetPath(configsGUIDs[0]));
    16.             }
    17.         }
    18.         #endif
    19.  
    20.         if (!s_Instance)
    21.         {
    22.             s_Instance = CreateInstance<DemoConfig>();
    23.             s_Instance.name = "Default";
    24.         }
    25.  
    26.         return s_Instance;
    27.     }
    If we're in UnityEditor, the path to the resource is get with UnityEditor.AssetDatabase.GUIToAssetPath() and used to load the resource with Resources.Load().

    I hope this (too) long topic helped some devs... Please tell about what you think of this pattern (good/bad practice, advantages, issues, ...), answer my questions, improve my solutions, suggest new ones, or just tell me if this topic was useful to you :)
     
    Last edited: Aug 29, 2017
    kufra1, AbsurdAndy and a774565715 like this.
  2. Baste

    Baste

    Joined:
    Jan 24, 2013
    Posts:
    6,194
    Loading the asset from it's path lazily is going to be a lot easier and faster:

    Code (csharp):
    1. private const string assetPath = "something/something";
    2. private static DemoConfig _instance;
    3. public static DemoConfig instance { get { return _instance ?? (_instance = Resources.Load<DemoConfig>(assetpath)); } }

    For assets like this, you generally need it to always exist, and you want there to only be a single one. You handle the case where it doesn't exist by just creating a fresh one in memory, and handle the case where there are several ones by picking the first one found. I don't like either of those. You're handling special cases that should really be errors!

    If there's no Config file, and your game needs one, creating a blank one will just cause a bunch of nullreferences in arbitrary code locations when you try to use the assets from the instance.

    If there are several Config files, that's a user error and you should either ignore the ones that are not placed correctly, or send error messages somewhere. Picking a random one is bound to cause confusion if you suddenly end up with two, so you're not solving anything.




    A side note: I'm really not fond of having the singleton instance be publicly available. The calling code shouldn't really care if the config it's looking in is a singleton. It just needs to know that it's globally accessible information. It also makes the API a bit cumbersome. So instead of this:

    Code (csharp):
    1. public class MaterialRegister : ScriptableObject {
    2.  
    3.     private const string assetpath = "Resources/MaterialRegister";
    4.  
    5.     private static MaterialRegister _instance;
    6.     public static MaterialRegister instance {
    7.         get { return _instance ?? (_instance = Resources.Load<MaterialRegister>(assetpath)); }
    8.     }
    9.  
    10.     public Material redMaterial;
    11. }
    12.  
    13. //Calling code:
    14. var smr = GetComponent<SkinnedMeshRenderer>();      
    15. smr.material = MaterialRegister.instance.redMaterial;
    16. // now we need to know that the MaterialRegister has an instance, and the call becomes way long.
    Do this:

    Code (csharp):
    1. public class MaterialRegister : ScriptableObject {
    2.  
    3.     private const string assetpath = "Resources/MaterialRegister";
    4.  
    5.     private static MaterialRegister _instance;
    6.     private static MaterialRegister instance {
    7.         get { return _instance ?? (_instance = Resources.Load<MaterialRegister>(assetpath)); }
    8.     }
    9.  
    10.     public Material redMaterial;
    11.     public static Material RedMaterial { get { return instance.redMaterial; } }
    12. }
    13.  
    14. //Calling code:
    15. var smr = GetComponent<SkinnedMeshRenderer>();      
    16. smr.material = MaterialRegister.RedMaterial;
    17. // There's a material register with a red material. Easy to read, no singleton visibility
     
    Deng-Jia, noio, john-wallace and 2 others like this.
  3. Peter77

    Peter77

    QA Jesus

    Joined:
    Jun 12, 2013
    Posts:
    6,438
    Edit/Update: Unity Technologies recommend to avoid using the Resources Folder for most things. Please see "Best Practices for the Resources System":
    https://unity3d.com/learn/tutorials/topics/best-practices/resources-folder

    The ?? operator does not work properly/consistently with UnityEngine.Object types, see this post.

    I submitted a bug-report (Case 824765) regarding the ?? operator on August 2016, but the bug-report was unfortunately closed with "not working by design". Here is the reply from Unity Technologies:
    I suggested to add an overloaded ?? operator as well, to make the behavior consistent at least. However, I was asked to submit this as a feedback item instead, which I didn't do yet though.

    In case anyone is interested in the test-project that I submitted to Unity Technologies, which shows that the ?? isn't working consistently with UnityEngine.Object types, here is the download-link:
    http://www.console-dev.de/bin/Unity5_824765_BugReport_Null_Coalescing_Operator.zip
     
    Last edited: Mar 23, 2019
    Polymorphik likes this.
  4. Baste

    Baste

    Joined:
    Jan 24, 2013
    Posts:
    6,194
    The ?? thing has been discussed a lot here. There's some long discussions here and a shorter update here. It's not possible to overload ??, so that's not an option.

    It's okay here, though. The weird Unity behaviour around ?? only happens if the object gets destroyed. Since it's an asset that contains stuff you need globally accessible, it won't be destroyed.
    If Unity reloads the assemblies due to a live recompile, it either:
    - fails to serialize the field, in which case it becomes "true" null
    - serializes the asset, in which case it's put back in as is, since it's an asset.

    So it'll work! In this scenario.

    I mean, you can destroy it, but that would be an insane thing that already breaks the game, so there's no reason to work around it, imo.
     
    Novack and noio like this.
  5. DaCookie

    DaCookie

    Joined:
    Nov 4, 2014
    Posts:
    44
    @Baste You're right, no problem is solved by pick the first asset of expected type, or creating an empty one... But the code I wrote before is an example.
    But we can count the assets and log errors or throw exception to clearly have feedback of any problem with that.

    I can't use simple string path as you suggested for 2 reasons :
    • I don't want my designers to open scripts or steal the time of a programmer to edit the path if they move or rename the asset
    • My "SingletonScriptableObject" class is generic... and I can't define a path without breaking that generic pattern
    So, taking advice of your reply, my class can be like :

    Code (CSharp):
    1. using UnityEngine;
    2.  
    3. public class SingletonScriptableObject<T> : ScriptableObject
    4.     where T : ScriptableObject
    5. {
    6.     private static T s_Instance = null;
    7.  
    8.     public static T Instance
    9.     {
    10.         get
    11.         {
    12.             if(!s_Instance)
    13.             {
    14.                 T[] objs = null;
    15.  
    16.                 #if UNITY_EDITOR
    17.                 // If we're running the game in the editor, the "Preloaded Assets" array will be ignored.
    18.                 // So get all the assets of type T using AssetDatabase.
    19.                 string[] objsGUID = UnityEditor.AssetDatabase.FindAssets("t:" + typeof(T).Name);
    20.                 int count = objsGUID.Length;
    21.                 objs = new T[count];
    22.                 for(int i = 0; i < count; i++)
    23.                 {
    24.                     objs[i] = UnityEditor.AssetDatabase.LoadAssetAtPath<T>(UnityEditor.AssetDatabase.GUIDToAssetPath(objsGUID[i]));
    25.                 }
    26.                 #else
    27.                 // Get all asset of type T from Resources or loaded assets.
    28.                 objs = Resources.FindObjectsOfTypeAll<T>();
    29.                 #endif
    30.  
    31.                 // If no asset of type T was found...
    32.                 if(objs.Length == 0)
    33.                 {
    34.                     Debug.LogError("No asset of type \"" + typeof(T).Name + "\" has been found in loaded resources. Please create a new one and add it to the \"Preloaded Assets\" array in Edit > Project Settings > Player > Other Settings.");
    35.                 }
    36.  
    37.                 // If more than one asset of type T was found...
    38.                 else if (objs.Length > 1)
    39.                 {
    40.                     Debug.LogError("There's more than one asset of type \"" + typeof(T).Name + "\" loaded in this project. We expect it to have a Singleton behaviour. Please remove other assets of that type from this project.");
    41.                 }
    42.  
    43.                 s_Instance = (objs.Length > 0) ? objs[0] : null;
    44.             }
    45.  
    46.             return s_Instance;
    47.         }
    48.     }
    49. }
    Now, once the asset is stored in "Preloaded Assets", the file can be anywhere in the project. And if the asset can't be loaded, you get an error and see what happens instead of getting a new "empty" asset.

    Your point about "singleton instance publicly available" is a good remark. Any inheritor of this class should have a static accessor to their members instead of using MySingleton.Instance.
     
    Last edited: Aug 29, 2017
    Vedran_M, Grumpy-Dot and a774565715 like this.
  6. Punfish

    Punfish

    Joined:
    Dec 7, 2014
    Posts:
    331
    I covered how to do this without relying on the resources folder:


    Skip to 9 minutes.
     
    VigorousApathy likes this.
  7. Baste

    Baste

    Joined:
    Jan 24, 2013
    Posts:
    6,194
    Please don't necro two year old threads to plug your Youtube channel.
     
  8. Punfish

    Punfish

    Joined:
    Dec 7, 2014
    Posts:
    331
    You mean, please don't comment on a top google result in an attempt to help people from making the mistake of using what could be a very intensive operation?

    Don't be so naive.
     
  9. Shack_Man

    Shack_Man

    Joined:
    Jun 7, 2017
    Posts:
    365
    Exactly how I just landed here :) I'm gonna check out your channel, looks pretty good.
     
    Punfish likes this.
  10. howler123

    howler123

    Joined:
    May 7, 2017
    Posts:
    17
    So I just found a easy fix, I am in the process of moving from Mono Singleton to Scriptable Singleton had same problem, I just added the files I needed loaded in the Project settings, Player Preload assets and all my NULL references disappeared.
     
    RogDolos likes this.
  11. Punfish

    Punfish

    Joined:
    Dec 7, 2014
    Posts:
    331
    I will check this out and give credit where it's due if it works, thank you!

    PS: I just realized this was posted months ago, but still! :)