Search Unity

Scene object references in ScriptableObjects / Prefab script components

Discussion in 'Scripting' started by uwdlg, Jul 8, 2020.

  1. uwdlg

    uwdlg

    Joined:
    Jan 16, 2017
    Posts:
    150
    After reading more about the Singleton design pattern and why it's mostly discouraged, I've turned to using
    ScriptableObject
    s which are "injected" into client
    MonoBehaviour
    s via drag'n'drop in the editor for manager classes. However, I haven't yet found a satisfying solution for when I need to reference a scene object / a way to properly design things so I don't run into that problem (at least as often as I do).
    I think it's best if I explain what has gotten me to this point this time around:
    To manage health bars, I started with a Singleton
    MonoBehavior
    (with a
    public static Instance
    field set to
    this
    in
    Awake()
    ) responsible for instantiating health bars from a prefab for each game object that needs one and updating it based on events from the game object. So an Enemy
    MonoBehaviour
    would retrieve the Singleton instance, register itself as a health bar "owner" and the Singleton would create the actual health bar, hook up events and so on. Creating the health bar requires a reference to the canvas in the scene to use as parent (One overlay canvas, not multiple World Space canvasses).
    Next I wanted to replace the Singleton with a
    ScriptableObject
    , but could no longer directly reference the scene canvas. That got me wondering about good ways to set up that reference, and I opted for
    GameObject.FindWithTag()
    from within
    OnEnable()
    which isn't great as it relies on the canvas object having the correct tag.
    Then I thought about not using a global manager for the health bars at all (because duh), but a separate
    MonoBehaviour
    added to each enemy instead. Referencing scene objects from enemies that are also scene objects obviously works, but I would have to set up the reference by hand for each one manually (or is there some better way?). Even worse the original problem still remains with enemy prefabs which can't have references to scene objects.

    Is there a nice solution for this that doesn't need some sort of Find() method that relies on the scene objects being set up correctly (with a tag / their name / their position in the hierarchy)?
    Looking forward to hearing how other people solve / dodge this.
     
  2. MaskedMouse

    MaskedMouse

    Joined:
    Jul 8, 2014
    Posts:
    1,092
    I've been experimenting quite a lot with structures. I tried ScriptableObjects based on Ryan Hipple's talk. But after the project grew larger and when addressables came into play, I instantly regretted using this structure. Due to using addressables some instances of ScriptableObjects were in the scene some on an addressable prefab. Which caused things to break because of different instances. I could fix it by moving everything to addressables but that made scene loading & editting a bother. Besides it being annoying with Addressables, I had too many events with too many variables it cluttered my project immensely. At some point I didn't know whether I could delete a SO, if it were in use or not. When I wanted to replace a SO, I had to replace them everywhere they were serialized. So then started using References to SO's. But then found myself I still had to replace all references to those references SO's... ugh. It went from one problem to another.
    For a simple project without addressables I'd say it's an ok structure but as soon as it grows larger it becomes tedious.

    Singleton pattern is not necessarily bad but you have to manage it well.
    I made peace with a mix of Singletons & Dependency Injections. But it all depends on what does your project need and what makes it manageable.

    for connecting a Player HUD with the actual Player Data there are so many different approaches. They're all valid and they all have their pro's and con's. But it is up to you as developer to choose one of those approaches as you see fit and what works for your project. Don't overthink it.

    You can have the player instantiate the UI and inject itself.
    You can inject your player data into the UI using the UI as Singleton.
    You can create a separate Player scene that loads additive and have UI & Player references serialized.
    You can connect the player and UI through a scriptable object which holds a reference to Player data.
    You can connect the player and UI through events / delegates
    * and probably many more ways

    Singleton pattern
    Dependency Injection pattern
    Serialization
    Scriptable Object References
     
  3. uwdlg

    uwdlg

    Joined:
    Jan 16, 2017
    Posts:
    150
    Thanks for the answer!
    I have yet to delve into Addressables, so I don't entirely understand the points about them (how can a ScriptableObject instance be part of a prefab?). I'm also not sure if I'm overlooking something here, but the problem with not knowing if a SO is in use should in my case be solvable using "Find references in scene", pointing me to scripts with serialized fields of that type (of course this gets tedious with references in other scenes). Having to change every reference by hand when replacing a SO is okay for me, as I don't (yet) have any that are used by more than 2-3 scripts, also I like having those dependencies explicitly represented/"injectable" by the serialized fields on the client scripts.
    For my exampe case, I've just come up with a "solution" that I might use:
    Having one singleton MonoBehaviour (
    Instance = this;
    in
    Awake()
    ) with serialized fields for all scene objects I need references to from within SOs in multiple scenes. I would then add that script to an empty GameObject in each scene and hook up the scene object references locally in those scenes. The SO retrieves any scene object references from the Singleton MB whenever the
    SceneManager#sceneLoaded
    event fires.
    To me that seems to be a nice solution for scene object references in SOs being used in mutltiple scenes.
    I've realized that my initial example is not a good one for this, because I don't think that every level scene should have its own canvas instead there should be one as a prefab or in a separate scene which is loaded additively. Using
    FindWithTag()
    is okay then since there is only one Canvas that needs the correct tag. A better example would be this:
    I'm using a SO manager script for spawning coins and pickups in a shoot'em up and need a reference to the transform of the world mesh to use as parent for the spawned object (I'm doing this because I move the world backwards instead of the camera forwards and this way the spawned objects move correctly as well, creating the illusion that both the player ship and the camera are moving forward). In this case I would use the same SO for spawning in different scenes (levels) with different world meshes so I believe my idea works for this.
    I'm realizing now that I'm still struggling with how to make things reusable between scenes which may lead me to overthink things.
     
  4. MaskedMouse

    MaskedMouse

    Joined:
    Jul 8, 2014
    Posts:
    1,092
    If you drag a prefab let's say the Player into the scene. That prefab has a SO reference for the Player HP.
    If you load a Health Bar using Addressables and that Health bar has the same SO reference for Player HP referenced.
    It will be a new Instance of that Scriptable Object. So when the Player changes HP it will not be changed in the UI because it is a different instance.
    As soon as an Addressable Asset is used, any Scriptable Object referenced in those assets, will be different instances.
    They are not the same as the Project referenced SO's anymore.

    Like I said, if a project grows larger it becomes less easy to manage. As for searching whether the asset is still in use, I used an asset from the asset store to search through all scenes & prefabs whether it is in use somewhere. Still, it 'fixes' the problem with using another tool to mask the problem.

    For me it were 20 ~ 30 references by hand (I think even 50), which at that point I used a GUID replacement tool to make that progress faster. Doesn't make it less tedious work.
    But it all depends on how complex the project is
     
  5. uwdlg

    uwdlg

    Joined:
    Jan 16, 2017
    Posts:
    150
    Interesting, thanks for the details. I can see how with those numbers Singletons with refactoring and other tools IDEs provide may be more convenient than SOs with manual work / more editor tools.
     
  6. MaskedMouse

    MaskedMouse

    Joined:
    Jul 8, 2014
    Posts:
    1,092
    Scriptable Objects can be useful, but for large projects I rather refrain from using them. But that is a personal preference.
    I rather have the compiler screaming its lungs out instead of having to find out that that one prefab or instance in the scene has lost a reference to the SO causing a nasty bug. Or having to write Assert.IsNotNull in every script...