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

Question Architecture with ScriptableObject when there are more than one instances

Discussion in 'Scripting' started by eterlan, Jan 6, 2022.

  1. eterlan

    eterlan

    Joined:
    Sep 29, 2018
    Posts:
    177

    and https://unity.com/how-to/architect-...93.1173597951.1641436975-269435373.1622515049

    all by @roboryantron, thank you!

    It sounds really attractive and natural to use ScriptableObject to store small things like PlayerHealth and some event. In this way data and system are dispatched clearly, and code can be test seperately with ease.
    But I found it becomes quite strange to do it when there are more than one instances, like enemies health, or enemy attack event. When I instantiate a new enemy prefab, all its scriptable obejct reference link to the original one. A workround I can think of is instead of Instanitate enemy with prefab, use a enemy Factory to create enemy by hand, and Instantiate all the event and variable scriptable object there. Now each component (different functionality) of new enemy instance have no idea of each other, dependency is injected by EnemyFactory, is it sounds good or a totally misuse of this architecture? Is there any better way to do this kind of things?
    Thanks in advance.

    ------ Edited 2nd ------
    Ryan actually fix the problem I mentioned with his implementation below, which is still data-oriented, and also editable at inspector for each instances with only one scriptable object, amazing! so I'm convinced.:)
    Also I found there are some cool open source projects in github by search with magic words "Ryan Hipple".
    https://github.com/DanielEverland/ScriptableObject-Architecture (seems deprecated sadly..)
    https://github.com/polartron/ScriptableObject-Instances (instances variables!)
    https://github.com/unity-atoms/unity-atoms (develop actively with 700+stars, and also support instances variables! )
    Thank you all! And hope this would be helpful for someone else interested.
     
    Last edited: Jan 8, 2022
    16845231 likes this.
  2. Lurking-Ninja

    Lurking-Ninja

    Joined:
    Jan 20, 2015
    Posts:
    9,913
  3. spiney199

    spiney199

    Joined:
    Feb 11, 2021
    Posts:
    5,895
    If you're using Scriptable Objects to say, design enemy health and other stats, I believe the standard procedure is to encapsulate that data inside a struct/class and copy that over to the instance of the enemy in awake/start. Alternatively you can Instantiate() the SO to make a copy of it.
     
  4. eterlan

    eterlan

    Joined:
    Sep 29, 2018
    Posts:
    177
    Thanks for you reply! The thing is not to make float field encapsulated, but create a float reference by scriptable object, and to use this reference in other systems. This way other system (or component) don't care about where the data come from or which enemy instance it is, but only the data itself.
     
  5. eterlan

    eterlan

    Joined:
    Sep 29, 2018
    Posts:
    177
    Thanks for your reply! Maybe I didn't describe well.. Actually my problem is this. Is it worth it when it come to many instances?
    Code (CSharp):
    1.     public class FloatVariableSO : ScriptableObject
    2.     {
    3.         public float Value;
    4.     }
    5.  
    6.     public class Enemy : MonoBehaviour
    7.     {
    8.         public FloatVariableSO Hp;
    9.     }
    10.  
    11.     public class EnemySpawner : MonoBehaviour
    12.     {
    13.         public GameObject EnemyPrefab;
    14.         private void SpawnEnemy()
    15.         {
    16.             // This doesn't work, because it doesn't create new scriptable object to populate the Hp field, instead it use the original scriptable object.
    17.             Instantiate(EnemyPrefab);
    18.         }
    19.  
    20.         public void SpawnEnemyByHand()
    21.         {
    22.             // Have to do it this way..
    23.             Instantiate(EnemyPrefab);
    24.             var enemy = GetComponent<Enemy>();
    25.             enemy.Hp = ScriptableObject.CreateInstance<FloatVariableSO>();
    26.         }
    27.     }
     
  6. spiney199

    spiney199

    Joined:
    Feb 11, 2021
    Posts:
    5,895
    The way I see it, creating instances/instantiating scriptable objects is no different than passing around values by reference (ergo classes), so I don't see much point in it (entirely personal opinion, of course). If you were to spawn an enemy, copy the values you care about, then pass the reference to that copy to any systems that care about it, it would be effectively the same as Instantiate()-ing a scriptable object asset.

    Doesn't mean you can't do it the SO way, nor does it mean you can't design with SO's as well. But when you're copying scriptable objects they do tend to hang around in memory unless you destroy the copies, rather than being garbage collected like normal POCO's would.

    Regarding your code above, yes just Instantiate()-ing the prefab won't make copies of the SO, in the same way it doesn't make copies of any other referenced assets. In either case you'll be copying the SO, or copying the data.
     
    eterlan likes this.
  7. Lurking-Ninja

    Lurking-Ninja

    Joined:
    Jan 20, 2015
    Posts:
    9,913
    My answer on the link means I don't see any advantage doing it. Usually. Unless you have some very good case. Creating extra classes and stuff just for this is prone for error. It doesn't give you the advantage to make game design easier. I would keep the runtime data in monobehaviours, that's easier to manage and even make available in the inspector to tweak if needed.
     
  8. eterlan

    eterlan

    Joined:
    Sep 29, 2018
    Posts:
    177
    Maybe you are right..
     
  9. eterlan

    eterlan

    Joined:
    Sep 29, 2018
    Posts:
    177
    I guess I found something interesting, the Open Project by unity official and community, initialize scriptable object value in Awake()
    Code (CSharp):
    1.  
    2. public class Damageable : MonoBehaviour
    3. {
    4.    [Header("Health")]
    5.    [SerializeField] private HealthConfigSO _healthConfigSO;
    6.    [SerializeField] private HealthSO _currentHealthSO;
    7.    ...
    8.     private void Awake()
    9.     {
    10.         //If the HealthSO hasn't been provided in the Inspector (as it's the case for the player),
    11.         //we create a new SO unique to this instance of the component. This is typical for enemies.
    12.         if (_currentHealthSO == null)
    13.         {
    14.             _currentHealthSO = ScriptableObject.CreateInstance<HealthSO>();
    15.             _currentHealthSO.SetMaxHealth(_healthConfigSO.InitialHealth);
    16.             _currentHealthSO.SetCurrentHealth(_healthConfigSO.InitialHealth);
    17.         }
    18.  
    19.         if (_updateHealthUI != null)
    20.             _updateHealthUI.RaiseEvent();
    21.     }
    22. ...
    23. }
    My mainly concern with factory code is I have to udpate those when I add new functionality in one place. But Actually I can simply do that in Awake for each new Monobehavior.

    ------ Edited ------
    Neven mind, this way there is no reason using Scriptable Object. The reason why I use Scritpable Object is just beacuse I can config the logic in the inspector, but when it come to the requirement to dynamically generate new instances, it's far more easy to just use simple float.

    And it's same for the scriptable object event. When it come to generate instances at runtime, UnityEvent in MonoBehaviour is much better as a decouple tool inside each instance..

    I think this whole scriptable object architecture is just fit to remove singleton pattern problem. The float is global "static" float, and the event is "static" event.

    Another problem is, if your enemy share some code with your player, like the "Damageable", you have to use ScriptableObject.CreateInstance for each enemy for the sake of player use this, and cannot gain any benefit from ScriptableObejct. Maybe you can avoid that by import another abstraction (interface IHealth { void GetHealth; ) .But it's not free for using this amazing configuration power..

    I guess I should still regard scriptable object as something like global value, it's not a right tool for multiple instances.
    ------ Edited 2nd ------
    Forget above.:p
     
    Last edited: Jan 8, 2022
  10. RogueStargun

    RogueStargun

    Joined:
    Aug 5, 2018
    Posts:
    286
    I think using a separate scriptable object for the health of each instanced entity is overkill and does not solve any specific design problem. Using SOs for the Players health on the other hand can be very useful as numerous systems can subscribe to the players health (ie UI events that trigger when the player dies and vfx effects). I think it's smarter to use scriptable objects to create event channels for things like player health and leave the actual floats alone.
     
  11. eterlan

    eterlan

    Joined:
    Sep 29, 2018
    Posts:
    177
    Enemy also has death event, ui health bar and attack vfx and so on..
     
  12. GroZZleR

    GroZZleR

    Joined:
    Feb 1, 2015
    Posts:
    3,201
    That's what the RuntimeSet is for in the original video, keeping track of collections of objects.
     
  13. eterlan

    eterlan

    Joined:
    Sep 29, 2018
    Posts:
    177
    But what about the scriptable object variable inside each element of set?
     
  14. GroZZleR

    GroZZleR

    Joined:
    Feb 1, 2015
    Posts:
    3,201
    I can't imagine Ryan was advocating to use the ScriptableObject variables for every single variable in your game. But the RuntimeSet concept allows your code to work in a similarly agnostic way, like the UI health bars for all of your enemies, without relying on finding GameObjects manually. I use a similar system in my game to track collections of components and it works brilliantly.
     
    eterlan likes this.
  15. eterlan

    eterlan

    Joined:
    Sep 29, 2018
    Posts:
    177
    Yeah.. That's pretty cool, it sounds like a linq function, I would give this a try later, thank you!
     
  16. roboryantron

    roboryantron

    Joined:
    Apr 2, 2014
    Posts:
    5
    Can confirm, Ryan did not mean to advocate using a ScriptableObject for every single variable in a script.

    Handling multiple instances is the most frequent question I get about the talk. There are some great solutions to this problem, but the first question you need to ask is how you will actually use that data. In many cases, it is just not worth pursuing the instanced path unless you have a lot of content driven pipelines in your game. If all you need is an HP meter on each enemy, instanced variables are probably too complicated.

    The primary way that my teams leverage instanced variables is when using our internal visual scripting system for things like a behavior tree, dialog tree, or some kind of content tool driven stat system (every weapon and enemy in Until You Fall had dozens of instanced variables).

    The way I have things set up is like this. There is a GlobalVariable scriptable object base type and a VariableDeclaration scriptable object base type. Things inheriting from GlobalVariable work like the examples from my talk. VariableDeclaration, however, can not hold a runtime value (it does define an initial value). It is meant to act as a key to retrieve a value from an Instancer.

    An Instancer serializeable can be added to a MonoBehaviour, like Enemy, to give them the ability to create their own instances of a VariableDeclaration. The instancer Drawer allows for the Enemy to define if it wants to override the VariableDeclaration initial value. On Awake, the Enemy initializes the instancer which will create a variable for each declaration and build a dictionary to more efficiently map a VariableDeclaration to a Variable at runtime. This way, a Behavior tree can have a node saying "If this enemy's instance of HP Declaration > 50, do x" where the HP VariableDeclaration can just be drag and dropped in to the node. You can use this for many other more complex examples, allowing designers to define their own variables and make nodes that read or write them.

    upload_2022-1-7_13-46-41.png
     

    Attached Files:

  17. eterlan

    eterlan

    Joined:
    Sep 29, 2018
    Posts:
    177
    I' m very excited to get reply from you handsome, thanks for sharing! Try hard to understand it like a lecture hhh. So if I get it correctly, you turn some unshared instance variable into a dictionary, and look up that variable with some instance id at runtime? So there is actually no multiple scriptable objects for enemy health like I thought before. Somehow it reminds me of how ecs work, grab the entity component value with it's entity id, also the AOS SOA stuffs. That's an elegant way, thanks again!