Search Unity

  1. Megacity Metro Demo now available. Download now.
    Dismiss Notice
  2. Unity support for visionOS is now available. Learn more in our blog post.
    Dismiss Notice

Why bother with ScriptableObjects?

Discussion in 'Scripting' started by Indie_Dev, Feb 10, 2018.

  1. Indie_Dev

    Indie_Dev

    Joined:
    Dec 29, 2017
    Posts:
    35
    Sorry if this question shines a glaring light on my inexperience, but I'm really struggling with the purpose for them given Unity's official "Pluggable AI" series of tutorials.

    Here's what I've taken away from those tutorials so far:

    * Create a class that doesn't need to be attached to game object *
    * Derive objects from this *

    In the Unity example, they create Scriptable Objects for States, Actions, etc. then use these to create assets, then attach all these assets to create the AI state machine. Okay, fantastic...

    What really is the difference in doing it this way and instead just creating standard classes, and attaching those?

    When it comes down to it, all of these assets created using Scriptable Objects are being attached to one another... Why not just derive from, reference, and attach MonoBehavior scripts themselves instead then?

    In some of the community's I've seen, Scriptable Objects seem to be held in high regard, but based on the limited info I've gleaned... they seem more like using a middle-man to accomplish the same thing...
     
  2. ThermalFusion

    ThermalFusion

    Joined:
    May 1, 2011
    Posts:
    906
    Regular classes can't be serialized by unity by reference, two serialized fields pointing to a single instance of a serializable class will serialize into a copy each, and become two different instances when deserialized.
    Similarly a derived class will only be serialized into the type of the field being serialized and will not deserialize into the derived type.
     
    DSivtsov and Owen-Reynolds like this.
  3. TonyLi

    TonyLi

    Joined:
    Apr 10, 2012
    Posts:
    12,670
    That's a good question. They are like using a middle-man. But you can hire a single middle-man to serve multiple customers, which is oftentimes more efficient.

    If you write a MonoBehaviour script and add it as a component to each AI-controlled GameObject ("agent") in your scene, then each agent will have a separate component. If the script allocates a lot of memory, each agent will allocate a lot of memory. If you tweak a value, it will only apply to that agent. This may be what you want, but very often you want to be able to tweak values globally. A common approach is to hold this global data and functionality (methods) in a ScriptableObject, and hold agent-specific data in a MonoBehaviour that references the ScriptableObject. As with the pluggable AI tutorial, you can plug different ScriptableObjects into the MonoBehaviour to produce different AI behavior.

    You could do the same with a plain class -- that is, a class that doesn't derive from ScriptableObject or MonoBehaviour. But there are advantages to using ScriptableObjects, as @ThermalFusion writes. Namely, Unity can serialize them, save them as asset files in your project, and reference those assets in other scripts.
     
    DSivtsov, TeagansDad and Ryiah like this.
  4. DonLoquacious

    DonLoquacious

    Joined:
    Feb 24, 2013
    Posts:
    1,667
    ScriptableObjects are, at their core, YAML data files. That makes them primarily data assets, like meshes or textures, but the kind and amount of data they hold is defined by the developer. None of the individual functions of ScriptableObjects are really unique to ScriptableObjects- vanilla classes can be used to do much of what ScriptableObjects do, MonoBehaviour-derived types can be drag-and-dropped as reference types in the editor, and data files are easily made in binary/JSON/XML.

    The combination of those elements, plus built-in support in the editor, is what makes them unique. You could make your own assets similar to ScriptableObjects if you like, there's nothing really preventing it, so this is mostly a case of "don't reinvent the wheel"- ScriptableObjects already exist, they do almost everything you could conceivably ask of them and can be extended to do even more, so they're the go-to method for quick scene-independent data storage.

    That AI tutorial series is great fun, but its really flexible usage of ScriptableObjects isn't really a good way to be introduced to them IMO- it makes them seem more like a kind of component system than a data storage system IIRC, and that's not really what I would describe as the "typical" usage. You're of course free to use them however you want, but for me personally, the less I see them treated as serializable data containers, the less reason I see for them to be ScriptableObjects at all.
     
    Last edited: Feb 10, 2018
  5. Kiwasi

    Kiwasi

    Joined:
    Dec 5, 2013
    Posts:
    16,860
    ScriptableObjects have the following advantages.

    - They can be modified via the inspector.
    - They can be handled with the usual unity Instantiate, Enable, Destroy pipeline.
    - They are quite friendly with the Unity serialisation system, and can be serialised via reference.

    If any of this sounds useful to you, then use a ScriptableObject. Otherwise use a vanilla C# class.
     
    Ryiah likes this.
  6. exiguous

    exiguous

    Joined:
    Nov 21, 2010
    Posts:
    1,749
  7. Suddoha

    Suddoha

    Joined:
    Nov 9, 2013
    Posts:
    2,824
    Personally, I think that makes them even more useful.

    It allows to not only seperate data from a component, but also pluggable logic like algorithms, which may in turn be associated with algorithm-specific values (those which may not be supplied by the consumer).

    The alternatives using MonoBehaviours or plain C# types is not always the best:

    First case: an algorithm that does not need any specific data
    • Implement all algorithms directly in that specific behaviour and use an indicator (e.g. an enum) to choose one
      • the algorithm's consumer potentially defines one or multiple algorithms that may need re-used in different places (not alot of flexibility, low reusability)
      • fixes, extensions and such need to be done in place, so you're touching code which may not need to be touched at all (bad maintainence, bad extensibility)
      • the algorithms may not be a concern of the consumer at all, however it defines the algorithms and knows the specific implementations (ignoring so many software principles, abstraction, coupling & cohesion ... you know it)
    • Implement the algorithms seperately, allow the component to choose one using an indicator like above
      • great (but not the ultimate) improvements with regards to re-usability and extensibility
      • consumer does still need to know about different algorithms or some provider type, i.e. it requires to add information when news algorithms or versions are added
    • Allow them to be set from outside
      • this is great, the consumer does only know the abstraction
      • does not resolve or request an algorithm itself
      • loosely coupled, highly cohesive
      • disadvantage so far: setting the algorithm in code, only DI framework or a proper programmatic build-up routine would solve this

    Let's re-iterate and add algorithm-specific configuration data.

    Second case: an algorithm that requires additional values, for instance an accuracy parameter, difficulty, iteration count..
    • Just like above, implement them directly
      • all flaws mentioned above
      • the class needs fields that are only a concern for specific algorithms, which is a really bad design decison and clutters the inspector, unless you start to write custom inspectors that switch between layouts
      • the previous makes it even harder to maintain, especially when you start to share the fields for different algorithms (proper initialization, resets etc required for every algorithm)
      • lacks the centralization of algorithm-specific configuration data (unless loaded from an external source, now this class would also need to tackle IO ... )
    • Implement them seperately, just like above
      • still struggling with the centralization of the data, unless loaded by the class itself
    • Allow everything to be set from outside
      • barely any problem, still requires the DI framework or the build-up routine to collect the data, configure the algorithms etc
    Consider the 3rd option, it seems to solve this well (imo).
    So the algorithm will be set and a configuration will be applied, both not being a concern of the actual consumer, how does an SO help here?

    The answer is as simple as it gets if you look at the usage of Data-SOs:
    Create a new AlgorithmA assets, configure the values and plug it in.
    Create another AlgorithmA asset, configure it differetnly for testing purposes and plug it in. No need to create an extra-prefab with all the associated components just for the sake of saving the previous configuration... because all the changes is part of the SO asset...
    Create an AlgorithmB asset, configure this one and plug it in to experiment with it...

    And so on.

    There's literally no change to the consumer except that you'll plug in another compatible algorithm that it'll use instead, without being aware of what it actually is and without holding additional information.
    This does also avoid "branching" which is often used so replace proper abstraction. And this one can get really annoying.

    In my opinion this is just fantastic.
    Of course, there's a major disavantage: It takes alot more effort to code that and find an appropriate architecture. It does also involve more classes so people who are not familiar with the code base may be confused at first

    It's something you are going to use when you want to create a highly maintainable and flexible code&asset base, if you have the time and patience for that.

    This is not the best option if you just wanna get something working in the most trivial way, for example when you need that particular code in one or a few places and there's no way you'd ever re-use it.
     
    Last edited: Feb 11, 2018
    ThermalFusion, neoshaman and Ryiah like this.
  8. xjjon

    xjjon

    Joined:
    Apr 15, 2016
    Posts:
    610
    A bit of a necro but rather than start a new thread, I will just continue this here to take advantage of the good discussion in previous posts.

    I wonder if Scriptable Objects are really that great of a data store.

    From the unity docs:

    "A ScriptableObject is a data container that you can use to save large amounts of data, independent of class instances."

    However, it seems that other alternatives such as YAML or JSON are better suited for data store as it is more flexible, scalable, and integrates well with third party tools. I believe in larger scoped games, the differences between more apparent, especially with designs where you have a local client / cloud data (i.e. mobile games, multiplayer, etc).

    With ScriptableObjects you get the benefit of being editor-friendly, lower cost of entry, great for prototyping, etc.
    Classes that directly depend on ScriptableObject / MonoBehaviour are also not as easily testable when compared to a `POJO` or `POCO` in this case. Common testing frameworks would require an interface between the SO and C# class, but with that middle interface, then the pluggable data may as well be serialized via one of the alternatives.
     
  9. xjjon

    xjjon

    Joined:
    Apr 15, 2016
    Posts:
    610
    There's a lot of points here so I will respond to just a few:

    disadvantage so far: setting the algorithm in code, only DI framework or a proper programmatic build-up routine would solve this

    Is this really a disadvantage? I don't think there are many arguments against using DI (especially from a design perspective the concept is very simple). There can be arguments made against specific frameworks, but generally I believe it's common enough to not be considered a "disadvantage". It's probably more of a disadvantage to not have DI.

    It allows to not only seperate data from a component, but also pluggable logic like algorithms, which may in turn be associated with algorithm-specific values (those which may not be supplied by the consumer).

    This is not specific to SO right? The same can be achieved with a POCO + plain old YAML datastore.

    For example, say we want to build a `MovementBehaviour` Component. This behaviour consists of a few pluggable behaviours such as PathFinding, Input, etc. Each of these pluggable behaviours may have their own associated values, and the base component may have values as well.

    So you would be able to construct this movement behaviour with something like:

    MovementBehaviour.Builder().WithPathFinding(pf).WithInput(input).WithBaseAttributes(attributes).Build();

    The parameters in this case can either be an SO or a POCO. I suppose in the case of a SO, you can "set" the field in the inspector via the editor.

    So with the non-SO approach, you would have a POCO for the pluggable algorithm, something like IInput -> PlayerInput, AiInput

    Which each can have it's own unique datastore values. So there wouldn't be cross-class data store of values that are unique to a single class. There's many different options as to how these objects are built, whether it's through a custom builder (like you mention, enum, class name reflection, etc) or through DI (easiest approach IMO).

    The benefit to having this flexibility is even more apparent if you look at a multiplayer approach, i.e. we need to build the behaviours for different players. In SO approach there could be a temporal SO to hold the data for each player, but in a POCO approach the temporal objects are more flexible and can be plug and play with building the behaviors.
     
    Suddoha likes this.
  10. Owen-Reynolds

    Owen-Reynolds

    Joined:
    Feb 15, 2012
    Posts:
    1,992
    Absolutely. Data stored in JSON with a common way to initialize and share is probably better than scriptable objects for projects where lots of people work on the data. But SO's are still fine. Unity has plenty of features which are good enough, but should definitely be replaced if you do more with them. Any given project will do 8 various things the straight-forward Unity way, using a superior (for this one use) custom solution for 2 others. Unity has never even tried to hide this.
     
  11. Suddoha

    Suddoha

    Joined:
    Nov 9, 2013
    Posts:
    2,824
    There's nothing wrong with your answer, of course you can do all of that in code, or with a DI framework. And it certainly makes sense to compose objects using various different approaches.
    However, when it comes to quick iterative testing and experimenting, like game balancing, or using some generators, styles etc, the SOs can be one of the essential keys for efficiency, especially when those guys working on it are not familiar with programming.
    Instead, there are (ideally) some fool-proof SO types you can tinker with, you can create multiple instances and set them up, a final drag&drop and voila - it's done. And that's all available through the editor - no need to access the code files, no need to edit external files, no need to worry about file-systems and namings.

    You're right, some things are better be done the one way, but SOs are definitely a thing designers love, if they're used properly.
     
    Munchy2007 likes this.
  12. Kiwasi

    Kiwasi

    Joined:
    Dec 5, 2013
    Posts:
    16,860
    These are great structures for storing static data. They are easy to write to a disk. They are easy to send via network. They can be made human readable. They can be read by different programs. And they are relatively resilient to structure changes.

    However they suck for dynamic data during actual game play. Do you really want to read and parse a hundred lines of data storage each time you update your characters health? This is where scriptable objects come in handy.

    My current workflow basically uses sciptable objects to load, manipulate and save JSON data from the disk/cloud. The game interacts with the scriptable object.
     
    Suddoha, neoshaman and TonyLi like this.