Search Unity

Scriptable objects for player data - Is there any point?

Discussion in 'Scripting' started by Cato11, Jan 18, 2022.

  1. Cato11

    Cato11

    Joined:
    Jan 5, 2021
    Posts:
    233
    I recently discovered this guidance by Unity, which advises that variables such as player data should be saved into scriptable objects. However I don't really understand why we would do this, since the negatives seem to outweigh the positives:

    Positives
    • Allows other classes to access the player data without maintaining a reference to the player
    Negatives
    • The data is persistent so needs to be manually reset during each play (for data that should be reset such as health etc - hence I am puzzled why Unity use this as an example above)
    • The accessing class still needs to maintain a reference to the scriptable object, e.g. by creating a public field and attaching it. Why is this better than having to maintain a reference to the player?
    • Tracking who has access to the data is more difficult, since you can no longer do a "Find All References" on the player class. You would manually need to look through each script, unless I am missing something?

    I fully appreciate that if you have multiple instances of an object that have shared properties, such as enemies, then using scriptable objects makes sense. Since you can detach the property and maintain it separately.

    But when it comes to single instance data that is not persistent, is there any point in using scriptable objects? Although Unity advises it... I cannot see one.
     
    MelvMay likes this.
  2. Lethn

    Lethn

    Joined:
    May 18, 2015
    Posts:
    1,583
    Coming from the perspective of the tutorial maker they're probably trying to instill some good practices for when you move onto larger projects that will require you to save data in the long run and access it in runtime and the like. I don't think you're wrong, but you have to bear in mind who this is aimed at, if you already have a lot of knowledge on games and how they're made as well as some very specific projects worked out where you don't need any of this code then it's your choice to ignore it for the sake of time saving.

    For instance though just to let you know it's not just you, there's a rather persistent and annoying trend I see on the unity forums of people trying to ask about overly complicated security on silly things like 'player score' in order to prevent people from editing files and so on to change things. I have to constantly ask why? Why waste time on such a pointless thing when you're trying to make a game and any protection is going to get cracked within five seconds by someone far more skilled than you anyway? These posters all seemed convince they had to get it done like it was the be all end all of their game which I find baffling.

    This is just an example but I thought I'd let you know it's not just you, with tutorials in general I have noticed too there's a bit of a trend nowadays sometimes to make things far too complicated as well for what they're trying to achieve. Programmers in general are guilty of this but I'm surprised to see people making 'beginner' tutorials and then just dumping a crap ton of code on people. I can see if you know how to do something simpler why it would confuse you, I'm kind of feeling that way with a lot of inventory code that I'm looking at on youtube. It seems though that tutorial makers and contributors are trying to strike a balance between good long term coding practice and getting beginners through their projects.
     
  3. MelvMay

    MelvMay

    Unity Technologies

    Joined:
    May 24, 2013
    Posts:
    11,486
    Agreed and the fallacy of one-size fits all is still alive and kicking.

    Also the "should be" often is implicitly "could be" with caveats. Posts like that often feel too lightweight and non-relevant/practical or overly complex for simple things. Always best to pick the useful bits out of it depending on your needs and experience.

    I think your post is valid for first asking the question with a critical eye and second, weighing it up with pros/cons. :)
     
    Cato11 and Lethn like this.
  4. Cato11

    Cato11

    Joined:
    Jan 5, 2021
    Posts:
    233
    Thanks for your comments Lethn and MelvMay, yes I completely agree. And I appreciate it is a difficult balance to strike as you say.

    In terms of myself, I am working on my first game so I am by no means an expert. This is why I read the guidance and try to learn the best practices. But I always try to consider it within my own scenario. If there is little palpable benefit and potentially added drawbacks, then it seems logical to ignore the guidance.

    The fact that you are not stressing the appropriateness of scriptable objects in this situation also make me think they are not particularly suited to it.
     
    Lethn likes this.
  5. spiney199

    spiney199

    Joined:
    Feb 11, 2021
    Posts:
    7,925
    I'm personally a fan of scriptable objects, and I can think of a few more pros in their favour:

    1: They're hot swappable. You can edit them on the fly, and swap out reference to them on the fly. I find this handy for testing.
    2: Serialisation is more stable. Prefab serialisation is a nightmare, and even the the Odin Inspector devs have thrown their hands up in the air and gone "You're on your own, bro". Custom serialisation in Scriptable objects is super stable, so you're almost relegated to using them when you need assets using special serialisation.
    3: Designer friendly. If anything I'd say scriptable objects are more designer friendly than prefabs, and 9 times out of 10 I'd user an SO over loads of prefabs, and just make one generic prefab that gets fed data by an SO. This works will with point 1, too, as you can easily swap out SO's to change the prefab on the fly during testing.

    Persistent data is a non-issue personally. It can be useful, in fact, having data be persistent between play sessions. It's just a facet of their behaviour you need to keep in mind, and most of the time I'll have an SO that has a partner monobehaviour container reset the SO's data in OnDisable/OnDestroy.

    I've seen the article before and it's basically a text version of that one Unite video about scriptable objects. Personally I think the 'FloatValue' concept is a bit of a trap, and you see new devs making hundreds of these SO's, when most of the time they could be making an SO that's a collection of data.

    Don't just have one SO for player heath, one for mana, etc... Have ONE Scriptable Object that contains all that data; and you can reinforce this by coding your own Scriptable Object singleton class to ensure there can only be one at design time.

    I have literally dozens of scriptable object classes in my project and there's many things I couldn't do without them. Items, level transitions, wrapper objects for scenes, scene managers, a manager for save files, instances of save data, nodes for a node system, character information... it goes on. And most of the time their creation is automated through editor scripts, so as to reduce the tedium of Right-Click -> Create -> Etc.

    Though their use does come down to what kind of dev you are, and what your preference/style is. Obviously, scriptable objects are my style.
     
  6. Cato11

    Cato11

    Joined:
    Jan 5, 2021
    Posts:
    233
    Spiney - Interesting points so thanks for sharing! The top three points are good to know but I don't really see how they apply to this situation, i.e. SOs for single instance data.

    This is precisely what I did last night as experimentation, I had a separate mono behaviour manually reset the scriptable object. I also packaged all of my player data (score, xp, health) into one SO to minimise the quantity of SOs.

    Where I am struggling is seeing the benefit of the "Singleton SO". It seems to me that SOs are useful for sharing data across instances. If I have one instance of a player, why not just have the data in fields within that instance? Having to create a separate Singleton SO just seems like unnecessary added complexity and another asset to maintain. Plus the problem of tracking who has access to it remains.

    Like you say, it is a question of style I suppose. I think in my case I will drop the use of SOs in this situation. I have another case where I need to create many instances of planet objects, which have some shared and some unique data. SOs seem perfect for this, so I will re-explore then.
     
  7. spiney199

    spiney199

    Joined:
    Feb 11, 2021
    Posts:
    7,925
    I agree on the tracking part, and would like Unity to have a solution to this. Though at some point I want to see if I can code my own solution with a bit of reflection.

    Not sure what you mean by 'sharing data across instances'. Each instance of an SO has its own data. Sometimes you just need one SO to hold the data you want. Ergo, player stats, save game manager, persistent config data for one of your tools, etc. Do you mean sharing data between instances that reference the scriptable object?

    But hey you may also want multiple instances of the players stats, so you can swap out and test different levels, stats, etc, quickly on the fly. Though usually I encapsulate the data, have one 'manager' singleton SO, then also 'instance' SO's which I can use to swap out data in the manager.

    I like scriptable object singletons as a sort of 'single-source-of-truth' combined with a manager tool. It also means I can edit the player's data without having to have the player's scene/prefab/particular component open. It's like an editor window without having to code the editor window.

    I should also note I never access a singleton scriptable object by it's 'Instance'. I always reference it directly like I would any other SO. The singleton part is to primarily reinforce that there can only be one source of truth, alongside the managerial aspect.
     
  8. I would like to point out the separation of concerns aspect of this. Let's say you have the Player MB and you have some UI. Normally, you would go about things like this: you write a MB on the UI side of things, which references the Player MB, read the data (usually polling in Update method, because every beginner does that...), great, you have your Player Health on the screen. Until you want to change things, for example change the Player prefab. Now you need to go around and reference the new player.
    One step better, you switch sides and reference your UI from your Player (or whatever handles Player's health), change the UI only when your player's health changes. Hurray, got rid of the polling in Update. Small win, but counts. The others still apply, if you change your UI, you need to update stuff. If you want to show your Player's statistics on multiple UIs, you need to reference every one of them and update them, except when they aren't shown, so you write a buttload of if or switch statements.
    You make an intermediate object, probably with some event system (SO is just one way, but it's convenient, because even your game designer can do it). You attach it to your player (update SO when player's health changed) and attach it to your UI (subscriber). Now, you have separation of concerns, your player only knows about your SO, your UI knows only about your SO, if any of those changes or gets replaced, the others (any number of them) don't have to be updated manually, from their point of view, nothing is changed.
    If you start to write tests, you no longer need to update your prefabs if you want more test cases, just make some extra datasets and plug them in from the test environment.

    Is it slightly more complex than simply update the UI from the player's code? Sure. But it's more robust too, write it once and then just works (well, most of the time). Also you don't have to keep in mind what references what. Everything is referencing the SO. End of story. So the initial question you have is simply doesn't matter anymore. Who cares what is referencing? You concentrate a smaller portion of your code at once, that is a big win.

    Of course, it gets a little get-used-to-it. Is it worth it? If any of the above seems like an advantage for you, then yes, if not, it's probably not. But it still remains a best practice, because of these things.

    (Side-note: you could do the same thing with a static intermediate class or whatever, the problem with that is that only those can access it, who reads code. If you're working alone, it's not a problem, but in a team where there are people, who don't read/write code, it becomes a huge advantage, it is not you who has to do the work, non-coders can wire up things on their own without code help and it goes a very long way).

    Edit, one more thing:
    The problem you're describing (single data, gets changed), it is only true in the editor. But still remains a problem. The proper solution to this is to have a small editor portion of your script which saves the value upon entering play mode and writes it back upon exiting play mode. So your data will behave like any MonoBehaviour. I also recommend a menu entry (checkbox) to be able to not to save back the previously stored value, so game designers can tweak values in play mode without losing it when they exit.
     
    Last edited by a moderator: Jan 18, 2022
  9. Cato11

    Cato11

    Joined:
    Jan 5, 2021
    Posts:
    233
    Yes that is precisely what I mean.

    Now this is interesting! This is indeed a positive that didn't occur to me. Definitely food for thought!
     
    spiney199 likes this.
  10. Cato11

    Cato11

    Joined:
    Jan 5, 2021
    Posts:
    233
    Indeed this is how I would tackle it without the use of SOs.

    Yes I see your concern. But not necessarily, e.g. if I change the appearance of the prefab does not mean I am changing the script component containing the data. I suppose it depends on how you are changing the prefab, but I think your concern is important to bear in mind.

    Yes I think this is the main benefit, which I sort of listed as the positive in my first post.

    Okay I see what you mean. But when you say "everything references the SO", that's not exactly true. There will be a subset of classes that care about data residing in that particular SO. So if you wanted to quickly check which classes those are, there is no quick way to do that it seems. It's probably possible as Spiney says but would require some manual work.

    I think this is an extremely valid point! And it resonates with what Lethn said at the start, about Unity encouraging practices that are scalable with bigger teams. In my case, I am working entirely independently, so perhaps I am over thinking by considering approaches more suited to larger teams. I suspect a static intermediate class is probably better for me, I didn't think of that so thanks!

    Really appreciate your insights. :)
     
    Lurking-Ninja likes this.
  11. The question is: why do you want to know that when you aren't working on all of those things which are referencing the SO? Are you changing something fundamental about your dataset? What can you change in your SO which requires you to think about the referencing parties: Class name, deleting the SO instance, changing the subscription method name, changing the type of the data. None of these happen late in development, so any objects referencing your SO are small in numbers, usually you still remember them. Besides, if you do any of these above, your compiler will scream like a pink piggy in the slaughterhouse, informing you that you changed something fundamental, we don't know what to do anymore. And those errors will tell you what referenced the SO.

    Additionally, BTW, I'm working on a solution which allows me to have a true shared SO even between enemy instances for example. Where, as you know, the shared data is a concerns since every enemy instance should have individual data (like current_health or something).
    The solutions to this are:
    - ditching the SO idea and storing the current_data type of things in MonoBehaviours (this is what I've been doing so far, only true shared data went to SOs, like starter_health and whatnot)
    - making separate SO instance for every enemy instance (obviously it's only feasible runtime), but then you lose all of the benefits of the SO (pluggable, event-driven, wire up in editor, etc)
    - or this one:

    I annotate every instance with a GUID. Both edit and runtime (so doesn't matter if I have a particular enemy referencing my SO in editor or created in runtime). The idea came from here.
    Then I just maintain a List (well, in runtime Dictionary) of values and as I discussed above, they get initialized by the value from the SO, which can be tweaked by game designers.
    And when the enemy instance is saying that "change my health", I ask for the GUID too and update only their values. Similar with subscribers, they need to discuss which stored value they need, they need to know the GUID. Using the system on the link (references by GUIDs) it becomes easy. And as a side-effect, I will have a system in-place to reference things between scenes too. Small extra win.
     
    Last edited by a moderator: Jan 18, 2022
    Cato11 likes this.