Search Unity

  1. Unity 6 Preview is now available. To find out what's new, have a look at our Unity 6 Preview blog post.
    Dismiss Notice
  2. Unity is excited to announce that we will be collaborating with TheXPlace for a summer game jam from June 13 - June 19. Learn more.
    Dismiss Notice

Resolved [SOLVED] Scriptable Object Comparison: Different behaviour in Editor vs Build

Discussion in 'Scripting' started by mdelacroix, Jul 5, 2023.

  1. mdelacroix

    mdelacroix

    Joined:
    Jul 1, 2022
    Posts:
    12
    Hello!
    First of all, sorry if my post isn't perfect, this is my first time posting here for help.

    Context: For a project I'm currently working on I decided to define UnitStats as scriptables objects (so it can contain icons, min/max values, etc...). I have a MovePointsDefinition (SO) which is used in the UnitDefinition (SO as well). UnitDefinition contains a Dictionary<StatDefinition, int> which defines the unit value for each stat (pretty self explanatory).

    When spawning a Unit (not SO) I'm iterating through the stats referenced in the corresponding UnitDefinition, and store the "current" value of the stat in a Dictionary<StatDefinition, int> (so for example i would have key = MovePointsDefinition and value = 10).
    In order to facilitate access to MovePoints through code (and possibly interpreted code later on) I decided to implement a MovePoints property. In order to access MovePoints value without a direct reference to the MovePointsDefinition SO, I decided to create a SingletonSO (yes I know) which contains references to the StatDefinitions I want to access easily (bascially a Database of hard referenced StatDefinition SO).

    Therefore, Unit has a Property MovePoints which basically says this:
    int MovePoints => this.statContainer[StatAccessor.Instance.MovePointsDefinition];


    This works perfectly in Editor, but fail dramatically in Build, as I'm getting a
    KeyNotFoundException: The given key 'MovePointsStatDefinition' was not present in the dictionary.


    I understand that the InstanceID are indeed different in Build, but I struggle to understand exactly why (I guess Unity is loading my MovePointsDefinition twice, even though it is a single Asset), and most importantly: is there a fix ? I initially thought Addressables could fix this, but I'm not very experienced with it and even after tagging my StatDefinitions, StatAccessor and UnitDefinitions as Addressables, the comparison still doesn't work.

    I know I could fix this by using a unique serialized GUID and compare it, or other less attracting solutions (such as using enums, which I don't want to). But I would like to understand perfectly why this is happening and is this just a bad usage of SO or is it a bug that might be fixed in the future.

    Also, I simplified the code here as I believe this issue is based on Unity's way of handling SO in Build vs in Editor, but know that there is a bunch of abstraction going on in this code, in case this could be the source of my problem.

    I can give further explanations or code snippets if needed.
    Thanks for the help!
     
  2. Yuchen_Chang

    Yuchen_Chang

    Joined:
    Apr 24, 2020
    Posts:
    129
    I'm not confident that if I understand the situation well, so I will just answer this question. InstanceID does not only differ in editor and build, but also differ in different instance. That is, if you close your editor and open it again, the InstanceID of your SO will change, since it is not the same instance.
    (Sorry I can't provide some solutions because I didn't understand the solution well)
     
  3. MaskedMouse

    MaskedMouse

    Joined:
    Jul 8, 2014
    Posts:
    1,093
    If you have a scriptable object asset referenced in the scene that is in the build list but have that same scriptable object also in an addressable group and being loaded in through script somewhere. Then they will be two separate instances of the asset.
    A built-in version and an addressable version.
    Anything you change at runtime to that built-in version won't change the addressable version and vice versa.

    In the Editor it loads the asset through the AssetDatabase which is the same instance as during playmode in the editor. Hence it works in the editor but not in a build.

    You'll need to either load it in via addressables at all times or keep it built-in at all times.
    Keeping it built-in at all times may prove difficult because no addressable scene or prefab may reference the asset.
    To keep it in addressables at all times would mean you cannot reference the asset in any of the built-in scenes.

    My solution to using a scriptable object project is to move everything to addressables. literally everything.
    I only have 1 built-in scene to update addressable assets and then boot up addressable scenes.
    Any scriptable object would then be in an addressable scene and thus only be the addressable version of it.
    I've got the folder with all the scriptable objects in an addressable group. So any newly created asset would be automatically added to the group.

    Addressables + Scriptable Objects is a tricky thing if you don't know how it works.
     
  4. mdelacroix

    mdelacroix

    Joined:
    Jul 1, 2022
    Posts:
    12

    Yes I do understand that InstanceID is base on the Instantiation, and that assets are Instantiated once in the Editor and stay as long as the editor is opened.
    What I'm refering to in my original post is that in Build, for the same ScriptableObject, which is only used via reference and never instantiated in runtime, unity give me 2 different instance IDs depending on the object accessing it.
     
  5. spiney199

    spiney199

    Joined:
    Feb 11, 2021
    Posts:
    8,148
    Mind you this behaviour isn't unique to scriptable objects. Any asset gets duplicated if you have it referenced in both addressable and non-addressable contexts.

    OP can easily see where assets get duplicated in the Addressables Analyze window. Ideally you should have zero duplications.
     
    MaskedMouse likes this.
  6. mdelacroix

    mdelacroix

    Joined:
    Jul 1, 2022
    Posts:
    12
    Thanks for the very clear answer!
    I understood everything. Indeed none of my scenes are addressables.
    Unfortunately, I do not reference my ScriptableObject in any of those scenes, only in other SO. Same thing for my StatAccessor, it is not referenced in scenes/prefabs. Same thing again for my UnitDefinitions.

    However, it might come from other SO containers (I have a SO which contains UnitDefinitions and this container is referenced in some prefabs in order to display it).

    So, following your tips, if I tag my scenes as Addressables as well as my Prefabs, it should solve everything?
     
  7. mdelacroix

    mdelacroix

    Joined:
    Jul 1, 2022
    Posts:
    12
    Sure thing, but the Addressables Analyze window shows no duplication (but again that might come from the fact that my Scenes are not Addressables, right ?)
     
  8. spiney199

    spiney199

    Joined:
    Feb 11, 2021
    Posts:
    8,148
    It should be. Referencing addressable scriptable objects in non-addressable scenes will cause them to be duplicated. This is true of any assets referenced by assets in the scene (all the way down the dependency chain).

    You have clicked on 'Check Duplicate Bundle Dependencies' and hit 'Analyze Selected Rules', correct?

    As mentioned, if you plan to use addressables you should organise all your assets and scenes into groups that make sense for your project.
     
    Last edited: Jul 5, 2023
  9. mdelacroix

    mdelacroix

    Joined:
    Jul 1, 2022
    Posts:
    12
    Yes I did, and no issues were found.
     
  10. Bunny83

    Bunny83

    Joined:
    Oct 18, 2010
    Posts:
    4,098
    How is your singleton implemented? So what does
    StatAccessor.Instance
    actually do? Is it a property or just a field that is set somewhere? Where and how do you load the SO at runtime? Is that in the Instance property getter? If not you would run into potential race conditions when it comes to the initialization of your objects.

    Sincetons should be lazy loading whenever possible. In case of preconfigured SOs they should probably be inside a Resouces folder and loaded through
    Resources.Load
    from inside the singleton getter in case it's not loaded yet.
     
  11. mdelacroix

    mdelacroix

    Joined:
    Jul 1, 2022
    Posts:
    12

    Here is my code for SingletonSo:
    Code (CSharp):
    1. public class SingletonSO<T> : ScriptableObject where T : SingletonSO<T>
    2.     {
    3.         private static T instance;
    4.      
    5.         public static T Instance
    6.         {
    7.             get
    8.             {
    9.                 if (instance == null)
    10.                 {
    11.                     AsyncOperationHandle<T> op = Addressables.LoadAssetAsync<T>(typeof(T).Name);
    12.                     instance = op.WaitForCompletion(); //Forces synchronous load so that we can return immediately
    13.                 }
    14.              
    15.                 return instance;
    16.             }
    17.         }
    18.     }
    you can see it is using Addressables.LoadAssetAsync
     
    Bunny83 likes this.
  12. mdelacroix

    mdelacroix

    Joined:
    Jul 1, 2022
    Posts:
    12
    And this is my StatAccessor

    Code (CSharp):
    1. public class StatAccessor : SingletonSO<StatAccessor>
    2.     {
    3.         #region STATS DEFINITIONS
    4.         [field: SerializeField]
    5.         public IntStatDefinition HealthDefinition { get; private set; }
    6.         [field: SerializeField]
    7.         public IntStatDefinition MovePointsDefinition { get; private set; }
    8.  
    9.         #endregion // STATS DEFINITIONS
    10.  
    11.         #region GETTERS
    12.         public static IntStatValueContainer GetHealth(StatContainer statContainer) => statContainer.GetStat(Instance.HealthDefinition);
    13.  
    14.         public static IntStatValueContainer GetMovePoints(StatContainer statContainer) => statContainer.GetStat(Instance.MovePointsDefinition);
    15.         #endregion // GETTERS
    16.     }
    with Unit accessing MovePoints like this:
    Code (CSharp):
    1.         public IntStatValueContainer MovePoints => StatAccessor.GetMovePoints(this.StatContainer);
    2.  
     
  13. Bunny83

    Bunny83

    Joined:
    Oct 18, 2010
    Posts:
    4,098
    Yes, that actually looks good.

    Though I barely used Addressables and I'm not sure how references to other stuff works exactly. If those other things are addressables as well, I would assume that it should work?!.

    I can't see how an addressable would reference things inside the project that isn't itself an addressable. Things inside a project are directly linked in the asset database.
     
    mdelacroix likes this.
  14. spiney199

    spiney199

    Joined:
    Feb 11, 2021
    Posts:
    8,148
    Here's a thought: did you build your addressable groups before building the game?
     
  15. mdelacroix

    mdelacroix

    Joined:
    Jul 1, 2022
    Posts:
    12
    Mh, nope. I thought this was automatically done when building the game.
    I'll give it a try. Is that through Build > New Build > Default build script ?
     
  16. spiney199

    spiney199

    Joined:
    Feb 11, 2021
    Posts:
    8,148
    No it's not, as sometimes you want to build your game executable without updating your addressables content.

    And yes, that's the way of rebuilding your groups.
     
  17. MaskedMouse

    MaskedMouse

    Joined:
    Jul 8, 2014
    Posts:
    1,093
    That actually depends on your addressable settings.

    upload_2023-7-5_14-5-23.png
     
    spiney199 likes this.
  18. spiney199

    spiney199

    Joined:
    Feb 11, 2021
    Posts:
    8,148
    Huh, well there you go. Probably should've searched that up before I made my comment.

    Nonetheless still something to check. My default global setting was on 'Build Addressables on Player Build', so perhaps another red-herring here.
     
  19. mdelacroix

    mdelacroix

    Joined:
    Jul 1, 2022
    Posts:
    12
    Mine is on "Build Addressables on Player Build"
    So I guess it was indeed automated
     
  20. mdelacroix

    mdelacroix

    Joined:
    Jul 1, 2022
    Posts:
    12
    Alright so right now i'm in the process of removing my scenes from built-in and making them Addressables. As MaskedMouse suggested earlier in the thread.

    Earlier I was only checking Fixable Rules - Check Duplicate Bundle Dependencies but scenes are listed in Unfixable Rules - Check Scene to Addressable Duplicate Dependencies (and this one is listing my StatDefinition as duplicates) so I guess that's the main thing to fix for now.

    Simple question but dumb: What should I do ? Have one "Splash Scene" that loads the others as Addressables ? and this one should be the only one listed in the build settings ?
     
  21. spiney199

    spiney199

    Joined:
    Feb 11, 2021
    Posts:
    8,148
    Yep that's pretty much the go to pattern to use with addressables.

    I like to call it my 'Bootstrap' scene, as I can do other initialisation in it at the same time.
     
  22. mdelacroix

    mdelacroix

    Joined:
    Jul 1, 2022
    Posts:
    12
    Update: Everything works correctly now :)
    I should have checked the Unfixable rules earlier.

    Thanks for the help, everything was very clear, I learned a lot.
     
    MaskedMouse and spiney199 like this.
  23. Bobane

    Bobane

    Joined:
    May 13, 2014
    Posts:
    17
    Just wanted to say that this exchange is one of THE most important threads on these forums, this should really be more accessible, thank you for the info