Search Unity

  1. Calling all beginners! Join the FPS Beginners Mods Challenge until December 13.
    Dismiss Notice
  2. It's Cyber Week at the Asset Store!
    Dismiss Notice

ScriptableObject References in Addressables

Discussion in 'Addressables' started by Colin_MacLeod, Nov 13, 2019.

  1. Colin_MacLeod

    Colin_MacLeod

    Joined:
    Feb 11, 2014
    Posts:
    111
    Hi there!

    I have some UI assets that aren't used all the time, so I would like to dynamically load and unload them using Addressables.

    These UI assets refer to various ScriptableObjects storing my game state.

    When I load the Addressable objects, it looks like they are creating new instances of the referenced ScriptableObjects - so they don't share the game state.

    Is there a way to tell these dynamically loaded components to all use the same (preloaded) instance of each SO?
     
    Kazko and bjarkeck like this.
  2. Colin_MacLeod

    Colin_MacLeod

    Joined:
    Feb 11, 2014
    Posts:
    111
    I guess the silence speaks volumes?
     
  3. Ramobo

    Ramobo

    Joined:
    Dec 26, 2018
    Posts:
    69
    Welcome to the Unity forum. This is probably not exclusive to here, but don't be surprised when you're downright ignored.
     
    Colin_MacLeod likes this.
  4. Jribs

    Jribs

    Joined:
    Jun 10, 2014
    Posts:
    71
    I will preface this by saying I have no idea, but that use case kind of seems like its against what addressables is for.

    If you need a universal game state that is populated by something from a bundle you should probably load up your scriptable object at the beginning and save the reference to it, then anything that needs to interact with it should target the references object.
     
  5. Colin_MacLeod

    Colin_MacLeod

    Joined:
    Feb 11, 2014
    Posts:
    111
    Hey, thanks for replying, @Jribs

    My universal game state is not populated from a bundle. I have some scriptable objects that live on the filesystem and are accessed by various scene objects.

    The issue is that if you take one of those scene objects and make a prefab out of it, and then instantiate that prefab using addressables, it seems the reference in the instance points to a new copy of the ScriptableObject - not the common one shared with the rest of the app.
     
  6. TextusGames

    TextusGames

    Joined:
    Dec 8, 2016
    Posts:
    182
    Referenced assets is copied inside each used bundle if they are not addressable themselves. Check dependency analyzer.
     
  7. Colin_MacLeod

    Colin_MacLeod

    Joined:
    Feb 11, 2014
    Posts:
    111
    @TextusGames Not sure I follow. I'm looking for some way to map the addressables to an existing ScriptableObject which is not an addressable (currently).
     
  8. Colin_MacLeod

    Colin_MacLeod

    Joined:
    Feb 11, 2014
    Posts:
    111
  9. Colin_MacLeod

    Colin_MacLeod

    Joined:
    Feb 11, 2014
    Posts:
    111
    @unity_bill I see you have posted replies to a few similar threads.

    Can you please chip in here? I don't want two copies of the scriptable objects - ideally, I want all my addressables to reference the original instances of the scriptable objects referenced by all the scene objects - not create a new instance of each for the addressables world.

    Using scenes (regular scenes, not using addressables) work in this way. If I reference the same scriptable object in two separate scenes, the data is shared between them.

    Is there a way to get the addressables for a given key to point to the original instance of the scriptable object?
     
  10. MaskedMouse

    MaskedMouse

    Joined:
    Jul 8, 2014
    Posts:
    407
    At some point I thought that using scriptable objects for shared states was nice to have but together with addressables it is a bit of a hell due to new instances of the scriptable object. So instead of sharing states using scriptable objects I reverted back to static singleton classes that contain the state. Since most of it is singleton anyway.

    So what I now tend to do is use scriptable objects strictly for copies of data only.
    i.e. if you have stats for a monster -> load in the scriptable object stats -> copy stat values to the instance of the monster -> unload the scriptable object.

    If converting references to static references is not possible or too much work, one workaround would be loading in the scriptable objects at the start and have any other object then load in the scriptable object by AssetReference as well.
    As long as the scriptable object isn't unloaded it should get the same reference.
    I had some trouble with that as well at some point. Now I just refrain from using scriptable objects for states at all costs as a result.

    bottomline: it sucks but thats just how it works.
     
    Colin_MacLeod likes this.
  11. RunninglVlan

    RunninglVlan

    Joined:
    Nov 6, 2018
    Posts:
    24
    I had similar problem with ScriptableObjects. Then I had one Global scene, which is supposed to always exist. It was a non-addressable scene, which loaded all the other Addressable scenes. As Global scene used the same Scriptables that were used in Addressable scenes, Scriptables were duplicated. I fixed this by turning Global scene into addressable too. Now I have one non-addressable GlobalLoader scene in Build Settings, which loads Global, and have no problems with duplicate Scriptables anymore.
    Problem with Scriptables appeared once more when I turned Prefabs into addressables. I then had 2 Groups, one with scenes, another with prefabs. Then I ran "Check Duplicate Bundle Dependencies" rule in Analyze Tool and created separate Group with dependencies that were previously duplicated in scenes and prefabs groups.
     
  12. Colin_MacLeod

    Colin_MacLeod

    Joined:
    Feb 11, 2014
    Posts:
    111
    @RunninglVlan

    Yes, that does sound very similar to my situation.

    Interesting. I've not done anything with scenes via Addressables - how do you tell Unity to fire up the first scene via addressables - usually that's just part of the build settings?

    Fundamentally, I don't really understand why there is a need for these extra instances in the first place? This all feels really counterintuitive. When I mark a Scriptable Object as addressable in Unity Inspector and give it a key, I expect that instance to be assigned the key, not a copy of that instance.

    @unity_bill why does Addressables make this copy in the first place? Is there a way to force the addressables system to assign the key to the same ScriptableObject instance as the rest of the app? If there were, I wouldn't have to worry about all this stuff, and I could refer to the same instance via an AssetReference in the loaded assets.
     
    Last edited: Nov 26, 2019
    RunninglVlan likes this.
  13. RunninglVlan

    RunninglVlan

    Joined:
    Nov 6, 2018
    Posts:
    24
    GlobalLoader is now the only scene in Build Settings. In Built game Unity loads it. Then GlobalLoader loads Global scene, and immediately GlobalLoader is unloaded. Afterwards I work only with Addressable Scenes. That's how it is if I understood your question correctly.
     
  14. Colin_MacLeod

    Colin_MacLeod

    Joined:
    Feb 11, 2014
    Posts:
    111
    So, is GlobalLoader just the name of your bootstrap scene, with your loader as a monobehaviour in that scene? Or is GlobalLoader some part of Addressable Assets I've not come across?
     
  15. RunninglVlan

    RunninglVlan

    Joined:
    Nov 6, 2018
    Posts:
    24
  16. Colin_MacLeod

    Colin_MacLeod

    Joined:
    Feb 11, 2014
    Posts:
    111
    Ah cool, thought so - just checking :)
     
  17. Paul_H23

    Paul_H23

    Joined:
    Jun 19, 2019
    Posts:
    17
    I had a similar problem, using ScriptableObjects all over the place, following Ryan Hipple's great presentation, and found that all addressable SO's were being duplicated, voiding the hole purpose of addressables if you ask me.

    I didn't find a suitable solution out of the box I'm afraid. I had to build a complex system of initialisation. I ended up using AssetReferences, but this didn't solve my problem either, as being asynchronous, I had no way of knowing easily when a reference was available to use, without flooding my code with tonnes of annoying code to check and get if not ready. In the end, I created a system based on marker interfaces. Every Monobehaviour (and some others, but that complicates it even further) that has an AssetReference to a SO implements this marker interface, and references the SO through a wrapper class. Then, at startup, a loader object is responsible for going through all objects that implement the marker interface and resolving their asset references. The final, and most annoying part, is that then all objects with any Monobehaviour that has these marker interfaces has to start out disabled, otherwise startup code that relies on the availability of the SO might run. I've done this by putting everything under a "Game Controller" object, that I turn off, and if I turn it on to do some editing, and forget to turn it off, everything goes pear shaped when I play, so I've gotten used to noticing that weird behaviour.

    It's a real pain, and I wish I hadn't had to spend so much time on something that seems like it should just work, but hey, onwards and upwards. The advantage is, I can use assetreferenced SO's throughout my code, without concern for whether they have been retrieved or not, and be quite safe in the knowledge that, as long as I follow the arcane rules, it'll work. Fingers crossed.
     
    Colin_MacLeod likes this.
  18. Colin_MacLeod

    Colin_MacLeod

    Joined:
    Feb 11, 2014
    Posts:
    111
    @Paul_H23 Spot on. Yes, that's pretty much exactly where I am. My approach is also based on Ryan Hipple's presentation from Austin 2017. It works wonderfully - until you try to use addressables.

    I experimented with AssetReferenceT a bit - hit the same asynchronous issues.

    My use case is specific: to dynamically load/unload parts of the app to conserve memory. It seems like a perfect fit for addressables, but as everyone is saying it's incompatible with the Richard Fine/Ryan Hipple approach, so I may have to go with small scenes loaded additively, each scene only containing a few prefabs.

    For me, the other option does seem to be to use addressables but maintain my own ServiceLocator style map of ScriptableObjects to address and replace the duplicate entries - but that seems messy and goes against the grain of the whole Dependency Injection architecture I'm using.

    Like you, it feels to me like this is something addressables should address "out of the box" - especially since we are both following well respected (and Unity promoted) architectural guidelines. Is there anyone else over at Unity I should be asking about how to approach this?
     
    Last edited: Nov 27, 2019
  19. MaskedMouse

    MaskedMouse

    Joined:
    Jul 8, 2014
    Posts:
    407
    There are many ways around it. But you should pick something that you are comfortable to work with.
    After I experimented heavily with ryan hipple's approach, at first I found it nice to just reference the scriptable objects but once the project grew larger and larger it became a hassle. Managing that many scriptable objects is not great at all.
    When implementing addressables it became even more of a hassle due to duplicate instances of the scriptable objects.

    At that point you have a few options:
    - Get rid of the scriptable objects and replace them with static class entries i.e.
    - Preload scriptable objects and reference them in a list somewhere so that additional loads take that same instance of the scriptable object. (that's how it should work but not certain)
    - Initialize them via a higher up management system to make sure that the scriptable object is of the same instance.
    Something like a dictionary which takes an asset reference as key and keeps the instance as value.
    When the key doesn't exist in the dictionary you load the instance and add it to the dictionary.

    It's hard to tell what to do without knowing your project. People can only give you some workarounds, but thats about it.

    In my case I just got rid of most scriptable object variables / references and replaced them with static class entries.
    Some of them I didn't replace and loaded them in in a bootstrap scene and then loaded the reference additionally in other scripts.
    Basically learned my lesson to use scriptable objects for data only and not for coupling systems together.
    When projects become pure DOTS based then this doesn't matter much anymore as everything is DoD based and not OOP based.
     
  20. Colin_MacLeod

    Colin_MacLeod

    Joined:
    Feb 11, 2014
    Posts:
    111
    Hi @MaskedMouse, well I can only speak from personal experience of course but over here the Ryan Hipple approach works well. I don't want to use singletons because that statically binds relationships - you need to change code to remap a reference to a subtype. Creating hard references between entities like this makes it difficult to test, isolate problems or evolve the design over time.

    With scriptable objects, the whole system is mapped out via inspector-defined relationships. This is a fundamental principle of inversion of control and one of its main benefits. Having worked on a few large projects like this, I have found it scales very well. I'm not sure what the hassles were that you experienced - but linking scriptable objects via the inspector was straightforward for us; it's been easy to swap out a single instance (or multiple instances).

    For these reasons, I don't see removing scriptable objects as a solution - they are not the problem, in our system at least.

    Most ways of using addressables with scriptable objects seem to involve a workaround - either a dictionary to preload & "fix" the references at instantiation time or using the service locator pattern possibly via AssetReferenceT. Both solutions lose this fundamental advantage of the Ryan Hipple / Inversion of Control approach (i.e. it's better to link components in just one place, in Unity Inspector).

    Again, I would side with @Paul_H23 to suggest this feels fundamentally like an issue that should be addressed in Addressable Assets. Scriptable objects are supposed to work the way they do everywhere else in the Unity stack. Of course, that's a design decision for the folks over at Unity. For our specific use case (remove assets dynamically to conserve memory), changing the whole architecture to use Addressable Assets doesn't seem to be the right trade off.

    If there is no way to assign the original instance of each scriptable object to a single Addressable Assets key, the best solutions for us are probably back to basics:

    - use prefabs and instantiate/destroy them directly
    or
    - use small scenes to group prefabs and instantiate/destroy those directly
     
    Last edited: Nov 28, 2019
    Kazko likes this.
  21. techmage

    techmage

    Joined:
    Oct 31, 2009
    Posts:
    2,081
    So just ran into this issue myself. Working on a solution and will post what I come up with.

    To me it doesn't seem entirely counter intuitive. Or perhaps I should say once I understood the issue it seems congruent with how everything works. An AssetBundle will grab a copy of all files it uses, unless it is using that file from another assetbundle. This seems to work, move your ScriptableObjects into an assetbundle and load them through that. But then the async issue, where I want to ensure my reference is set up in Awake so I can start using the object in Start.

    I am thinking that one should differentiate between 'Service' scriptable objects and 'Datum' scriptable object. Where Datum just holds data, like a color or a float. Then a 'Service' holds state some kind.

    I made this here to Pool loaded addressables:
    https://github.com/rygo6/GTPooling

    My current thinking is to add a 'AddressableServicesPool', where you can prepool a bunch of AddressableServices, then retrieve them synchronously in Awake.

    Using Addressable for services does have the odd benefit that you could theoretically update the serialized data of a service by updating the assetbundle for it.
     
  22. techmage

    techmage

    Joined:
    Oct 31, 2009
    Posts:
    2,081
    I updated that repo. My solution is a 'ServiceReference' and a 'AddressableServicesPool'. The Pool will load all the services in the begining and pool them. Then a ServiceReference has a path that lets it retrieve them synchronously in await. Basically this kind of ends up just using my pooling system like a Service Locator.

    For the purpose of IOC, this oddly is like even more IOC. Because your services are decoupled. Then your services are loaded via Addressables, so you could switch the service that everything points to via addressables.

    Inside my open source codebases I also have a ComponentContainer service scriptableobject which you can use as a ServiceLocator for components rather than FindObjectOfType.
     
  23. Kazko

    Kazko

    Joined:
    Apr 2, 2014
    Posts:
    17
    I am also going through the hell of migrating to Addressables with a project heavily based on SO Architecture. My problem is somewhat similar so I decided to post it ... maybe this will help investigate further.

    @techmage @Colin_MacLeod

    Rephrased issue based on further findings (see Edits):
    Instantiating ScriptableObjects referenced by Addressable scenes does not produce a full clone when in Use Existing Build mode. However, full clones are produced if the scene referencing the originals is not addressable.

    -----
    (EDIT 2 - turns out, the cloning works as it should in build! So, the only mode where the SO is not cloned properly is Use Existing Build. Use Asset Database, Simulate Groups, as well as final build - the cloning works as expected.)

    (EDIT - I realized that the scene difference in fact is that the scenes are referencing different instances of the SO (as was explained earlier). However, in this case, both instances seem identical (in terms of data), until you clone them. Cloned instance referenced by Scene in build, is a perfect clone. Cloned instanced referenced by a scene in addressables, is incomplete. Why is that?)
    -----
    Original post:

    The issue seems very complex and there are many combinations to test, so maybe I'm not getting this right at the moment, BUT, after couple of hours since "why nothing works out of a sudden" moment, the issue could be described as:

    Instantiating SO assets from components in Addressable scenes clones them in a very shallow way (basic fields retain values, but custom classes do not). Rephrase for clarity: a monobehavior component instantiates SO (that is addressable). If that component is in a scene that is also addressable (loaded by Addressables.LoadSceneAsync), the SO copy is incomplete. If the same component is in a scene that is not addressable (it's in build), the cloned copy is perfect. Also, this happens only using Play Mode Script - Use Existing Build.

    Test Setup: I have tested in a playmode session which has three scenes.
    - First is loaded at start, as it's index 0 in build. The component instantiates addressable SO (by direct reference), and the copy is a perfect clone.
    - Next, Addressable scene is loaded async with the same component. This time, the SO clone is incomplete.
    - Finally, another "build" scene (with index 1) is loaded by scenemanager. Again, the SO clone is perfect.

    Aside from the fact that it would be perfect if it behaved the same way, this is counter intuitive. I have moved my scenes to addressables based on analyzer, as when done so, duplicate dependencies were resolved.

    Haven't tested in Build, only editor, but I presume it should behave the same way when Using Existing Build.
     
    Last edited: Dec 2, 2019 at 2:48 PM
  24. Colin_MacLeod

    Colin_MacLeod

    Joined:
    Feb 11, 2014
    Posts:
    111
    @Kazko Sounds a little different to my situation. I really just wanted to find a way to index objects that should be loaded/unloaded, to conserve memory.

    @techmage Sounds like a solution. But it's really nice to be able to just edit scriptable objects in the inspector at runtime, and have those values persist. Don't you lose that if you decide to instantiate everything using addressables?

    In the end, I removed the addressables and rolled my own "Dependency Manager" that maintains references to prefab instances and destroys them when they are no longer referenced by any other component.
     
    Kazko likes this.
  25. techmage

    techmage

    Joined:
    Oct 31, 2009
    Posts:
    2,081
    @Colin_MacLeod This is where it's actually kind of interesting. If you set it to use "AssetDatabase" to simulate AssetBundles, then it uses the ones in the actual project, so they save when you stop playing. If you set it to use built AssetBundles, then it loads the SO from the bundle and it is a clone, thus you lose changes. This in a back-handed way actually gives you the ability to turn saving of SO changes on and off.

    I think some more classes need to be refactored in my GTPooling lib, but the way it ended up working so far I think actually brought more pros to the table compared to just using SO's directly.

    - Settings on your Service SO's can now be updated in a production build by pushing new assetbundles. This kind of ends up being a kind of 'LiveOps' solution.
    - You can toggle the behavior to have SO's reset on play/stop by switching it between using bundles or assetdatabase.
    - You can change the Service SO all your components reference by changing it in addressables, and not having to drag a new reference into all your components.
    - You can use the AssetReference UI to select your desired references on components. This is one of the things I wish to further refactor, as I just inherited the AssetReference UI. But I'd like when you make a 'ServiceReference' it would limit it to only addressables marked with 'Service'.

    @Kazko I think your understanding of it is actually a bit more intricate than it needs to be. It's pretty simple. Anything referencing a SO which gets put into a Bundle will clone the SO. If things from different Bundles, or from outside a Bundle need to reference the same SO, then that SO must be seen as a dependency and loaded from an external shared location.

    I would actually expect you could set this up so all SO's go in a shared dependency bundle that would get auto-loaded first. I haven't played with that yet though.
     
    Kazko likes this.
  26. Kazko

    Kazko

    Joined:
    Apr 2, 2014
    Posts:
    17
    @Colin_MacLeod Yeah sorry, didn't mean to derail, but first I thought it's a referencing problem, and it turned out to be something else.

    @techmage Thanks for the reply. If I may ... perhaps you can help if I explain a bit more. I do have every SO in a group, and analyzer says there are no issues. But this turned out to be a different problem than dependency clones. What I found out is that instantiating SO at runtime sometimes does not produce a full clone of the SO. There are very specific conditions when this happens.

    First, it only happens when Use Existing Build Play Mode Script is used. In other words, a game logic based on instantiating SOs suddenly stop working. Everything works in the other two modes, even in build, but not in the mentioned play mode. This seems like a bug, or I am not understanding something.

    And second - this is where it gets interesting - this only happens when a component that is doing the instantiation of the SO, is a part of a scene that is addressable. If the scene is not addressable, the component produces a proper clone. For the record, I checked that in both scenes, the actual referenced SO has all the data. But it loses some of the data when instantiated from a scene that is addressable.

    What I'm saying is that it should not matter if the scene is addressable (nor it should definitely not matter if we're using Use Existing Build script), instantiate should produce a proper clone.
     
    Colin_MacLeod likes this.
  27. Colin_MacLeod

    Colin_MacLeod

    Joined:
    Feb 11, 2014
    Posts:
    111
    Ah - of course! That makes sense! Yes, the AssetDatabase mode is just simulated, so it'll just see the disk-based asset.

    This is good stuff - I hadn't thought of that. Think we're too far along on this project to reengineer everything (or all SOs at least) to be addressable, but I do see how that could work really well. Nice!
     
  28. techmage

    techmage

    Joined:
    Oct 31, 2009
    Posts:
    2,081
    @Kazko maybe I didn't quite understand. I also don't quite follow when you say a "proper clone". If you instantiate something, whether a prefab or a SO, the only state that gets carried over is serialized state. So anything marked with [SerializeField], this is expected behavior.

    When you say "In other words, a game logic based on instantiating SOs suddenly stop working." I assume you have a SO which is storing state in some fields or properties, and these values are changing and supposed to be shared to many things which reference it. It would break because the things referencing that SO are actually referencing different instances of the SO. Call Debug.Log(SO.GetInstanceID) on your things referencing the shared SO to make sure its actually the same instance.

    If your referencing an SO directly in an inspector field, then that component gets put into an AssetBundle, it will make a copy of that SO. When your AssetBundle loads you somehow have to make that field reference the common shared SO.
     
  29. Kazko

    Kazko

    Joined:
    Apr 2, 2014
    Posts:
    17
    @techmage Thanks for the reply. I tried to reproduce this in a clean project and have found out that the problem I'm having is I'm using Odin Inspector. Now it's clear that Odin's special serialization is not carried over to the cloned SO. But still, only in that particular play mode and only in addressable scenes. Thanks for your patience, really appreciate it. Now it's time to take this to Odin's devs to see if it's a bug or some weird limitation. I would understand more if it didn't work at all, than to have these special corner cases, if you know what I mean.