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. Voting for the Unity Awards are OPEN! We’re looking to celebrate creators across games, industry, film, and many more categories. Cast your vote now for all categories
    Dismiss Notice
  3. Dismiss Notice

Best way to represent players' game data in each level?

Discussion in 'Scripting' started by oldnewuser, Jan 17, 2018.

  1. oldnewuser

    oldnewuser

    Joined:
    Jan 15, 2018
    Posts:
    25
    Hi All,

    I am new to scripting so have a very simple scripting question.

    I have a game that has, say, 100 levels, in each level, I want to store the following data:

    1. a bool: "unlocked" - whether this level is unlocked
    2. a byte: "stars_earned" - maximum stars earned by the player in this level (0, 1, 2 or 3)
    3. an int: "times_played" - number of times the player has played this level

    Now I have a GameManager to manage all affairs related to my game.

    What is the best way to represent the above variables for each of the 100 levels?

    creating 100 bools, 100 byes, 100 ints, sounds foolish... any method suggestions? Any suggested ways to implement it? Would appreciate a short script sample. Thanks a lot.
     
  2. Suddoha

    Suddoha

    Joined:
    Nov 9, 2013
    Posts:
    2,824
    Write a custom data type and make an array / a list of that type. You could either use a struct or a class.
     
    oldnewuser likes this.
  3. Brathnann

    Brathnann

    Joined:
    Aug 12, 2014
    Posts:
    7,140
    A simple json file to store it would be a good idea. Then you could save the json string to a file or to playerPrefs. You'd still need a custom class to use with it, but it makes it pretty simple to handle.
     
  4. LeftyRighty

    LeftyRighty

    Joined:
    Nov 2, 2012
    Posts:
    5,148
    a common unity newbie misconception is that every script is a monobehaviour.... you can have "poco" c# classes to do "normal" c# things like act as data structures, they just can't interact with the unity engine (no start/update etc.).
     
    oldnewuser likes this.
  5. Brightori

    Brightori

    Joined:
    Sep 15, 2017
    Posts:
    64
    U need to use ScriptableObject. Google it and watch videos.
     
  6. Brathnann

    Brathnann

    Joined:
    Aug 12, 2014
    Posts:
    7,140
    Yes, I use data classes for json to deserialize to. They don't require monobehaviour. :) Json was just a suggestion, as there are certainly tons of ways to do what they want.
     
  7. Suddoha

    Suddoha

    Joined:
    Nov 9, 2013
    Posts:
    2,824
    Certainly not.
    Assets are not meant to be persistent data storages for data that's being generated and gathered over time, such as player data...
     
    DonLoquacious likes this.
  8. Brightori

    Brightori

    Joined:
    Sep 15, 2017
    Posts:
    64
    he want to store level information, lock|unlock , rating etc

    i think better way have ScriptableObject with public list inside;

    List <LevelInfo> leveldata;

    class LevelInfo
    {
    name or id
    bool complete;
    enum stars;
    times_played;
    etc

    }

    ScriptableObject perfect to store all this data. U can ez add method for complete lvl

    public void SetLvlComplete (int id)
    {
    leveldata[id].complete = true;

    }
     
  9. pslattery

    pslattery

    Joined:
    Apr 10, 2017
    Posts:
    4
    To answer your question, as has been said, you want a data type that you can Serialize. You'd could then store 100 of them in an array, Serialize the whole array, and store that in a file. This incurs a bit of overhead as you probably don't ever need all 100 at once. You could store each of them in a separate file, in chunks (maybe you only see 20 on screen at a time?) etc.

    The key flow is going to be:
    1. Load data into memory (Deserialize the text file into actual instances of your data class)
    2. Operate on that data (determine which levels a player can access, update the number of coins collected etc.)
    3. Periodically Serialize and Save the instances of your data class to file/files.

    One big thing to think about is how debuggable this code is. Do you want to be able to access all the levels during development? Or is it important to be able to see the game at different states (say when levels 1-10 are unlocked)?
    These questions lead us towards a couple options. In my experience, this is where I'd start thinking about using ScriptableObjects, not to store the saved data, but to store the data to be saved.

    This is not true. The only kind of data you absolutely cannot store in a ScriptableObject is data you want saved between sessions. ScriptableObjects can 100% be used to track state, as long as you don't expect that state to be maintained after the application closes.
    I use ScriptableObjects as scene initializer data objects, that is, objects a scene looks at to know where its coming from, and how it should set itself up. These data ScriptableObjects are updated at runtime by the preceeding level (or if I'm debugging, manually by me so I can jump to a different state of game).

    So, in the case of tracking which levels are unlocked, this isn't the best use as that will probably be saved data. On the other hand, that data is going to be updated as the player plays (I'd expect a player to collect stars by playing a level). You could store this info in a ScriptableObject which is referenced by the Save function. This decreases coupling (the two systems never directly interact) and increases the reusability of your Save function (it just serializes a ScriptableObject, doesn't care whats in it), makes it more extensible (have a new thing to track? Add it to the ScriptableObject. It will automatically be saved and loaded), and makes it possible to debug in the editor.
     
    Brightori likes this.
  10. Suddoha

    Suddoha

    Joined:
    Nov 9, 2013
    Posts:
    2,824
    That's why I said persistent data in combination with the other bold part . What I meant by that is, they're not supposed to be used as kind of "save games" (that would have been a better wording - my bad).
     
    Last edited: Jan 18, 2018
  11. DonLoquacious

    DonLoquacious

    Joined:
    Feb 24, 2013
    Posts:
    1,667
    ScriptableObjects are not a good solution- they have little value for mutable runtime use because you have to save and load them in an identical manner to every other serializable object type (manually). There's no additional functionality they provide that makes them useful for holding data that needs to persist between runtime game sessions- all of their extra utility as a Unity construct is lost when the game is built, making them good only for holding data that needs to persist immutably from the Editor to Build (static data, like object identities, names, descriptions, etc).

    I love them dearly, and use them like they're going out of style, but mutable runtime data isn't where they shine.

    Likewise, PlayerPrefs are not a good solution in most cases. They're called "preferences" for a reason, and this is because the data is easily lost. On Android and iOS, clearing the game's cache will wipe out anything saved in PlayerPrefs, as will some cleaning tools on PC- there are some system setups in which no data saved this way would persist beyond the session, making it useful only for things like audio volumes, selected resolutions, and other things that can be trivially replaced and cause only a minor inconvenience to the player if lost. If it can't be trivially replaced, then PlayerPrefs isn't the proper place for it- this is why whenever people mention serializing to JSON and storing in a PlayerPrefs string, it makes me cringe.

    If you need mutable data persistence at runtime, you need to store that data to the disk as a file (or to a server, in the case of "always online" options). If you need something mostly identical to PlayerPrefs, but reliably persistent and which can be saved to the disk, let me know- I've got a class laying around called LocalPrefs that mimics it pretty well, if I can dig it out. Other than that, just create any normal C# class, fill it with a list of LevelProgress objects with the data you need defined in those, mark it as Serializable, and save and load it from the disk in any manner you like (JSON, binary, XML, YAML, etc).
     
    Last edited: Jan 17, 2018
    greatastrocow and Suddoha like this.
  12. pslattery

    pslattery

    Joined:
    Apr 10, 2017
    Posts:
    4
    You aren't wrong, but I would say two things:
    1. Having the ability to manually save and load your files means you have greater control. You know exactly when processing time is being taken up by your save/load functions.
    2. You're discounting all the benefits you receive from them at development time, which are potentially significant. Having the ability to modify data like this in the editor, at play time, is really useful for debugging, testing and finding edge cases. It also offloads some of that responsibility to designers!

    So while no, ScriptableObjects, at runtime don't really differ from other Serialized assets, in my experience, the benefits from using them for these reasons (I'm not advocating all ScriptableObjects, all the time) to your development process are usually worth it.

    This still means you'd have to go through what you describe in your last paragraph, of manually saving and loading, but now there's this intermediary tool you can use as I described in my first post.
     
    DonLoquacious likes this.
  13. DonLoquacious

    DonLoquacious

    Joined:
    Feb 24, 2013
    Posts:
    1,667
    I went ahead and dug out the LocalPrefs script I made and uploaded it to Pastebin here, if anyone's interested. There are a couple things to note though.

    First, I just added the two static functions to the end that handle saving and loading, and crammed more into the Instance property to make sure you're getting the version you need, when you need it. I had to do this because I normally have other systems that handle saving and loading of all objects in a central location, but wanted a "complete" example of how the class works in one file here. LocalPrefs are normally not stored in their own unique file, but rather in a large binary blob with a dozen other objects, saved and loaded collectively at given times.

    In this class, there are two distinct paradigms- editor usage and build usage. In the editor, the single, solitary LocalPrefs ScriptableObject is stored in "Resources/LocalPrefs.asset", so it essentially gets used like a singleton object and auto-loads itself when needed. This allows you to use LocalPrefs in editor scripts, and the values can be changed and saved with little effort (you will need to call EditorUtility.SetDirty(LocalPrefs.Instance) and AssetDatabase.SaveAssets() manually from the editor whenever you make changes though).

    In a build though, the Resources version of the asset becomes static data- it can no longer be changed as an asset without saving and loading it from elsewhere. It's still useful though, as long as you understand that. In this case, it switches from automatically loading the Resources version, to trying to load a local version from the PersistentDataPath, and again treat that like a singleton asset. It'll only load the Resources version if there's no local version yet, treating it like a "default" set of values.

    As pslattery mentions, this is only one of the ways to make the transition from editor-utility to runtime-utility. In his examples, he uses them as state containers for initializing things in a specific way. For instance, using LocalPrefs, modifying the asset, or making several different modified versions, to represent given program states and have the scene load itself around that state data. Because it's a ScriptableObject, you can very easily and arbitrarily define states by changing the data in the inspector to reflect the game state you want to see / test out. This is incredibly useful for debugging, so there is definitely utility there, and there's something to be said for the consistency of using the same object type in builds as you do in the editor, even if most of the functionality falls off in the transition.

    ScriptableObjects don't differ from any other serializable object types at runtime, and they lose most of their utility, but that isn't to say there's a reason not to keep using them as long as you save and load them yourself at that point. My point in my earlier post was more to do with expectations- beginners will dig into ScriptableObjects and have a certain expectation for utility that ceases to exist as soon as a build is made, and then get confused about why it's not working. This is especially painful if they've put a ton of effort into their systems with the mistaken belief that ScriptableObjects, or more specifically the AssetDatabase, is going to handle the saving and loading for them. This is a really really important thing to keep in mind- they are no different from any other serializable class in builds.

    My comments about ScriptableObjects being ill-suited was perhaps phrased badly- they're ill-suited if you expect any sort of special functionality out of them that makes runtime persistent data easier to manage- if you're already using them for editor tasks though, there's no particular reason not to keep using them in the builds.
     
    Last edited: Jan 17, 2018
    Suddoha and LeftyRighty like this.
  14. oldnewuser

    oldnewuser

    Joined:
    Jan 15, 2018
    Posts:
    25
    Thanks, it sounds like the easiest way for a newbie like me!
     
  15. joshcamas

    joshcamas

    Joined:
    Jun 16, 2017
    Posts:
    1,268
    Correct me if I'm wrong, but I would say the best reason why scriptable objects involved with saving can be soo powerful is because only scriptable objects support polymorphism while a normal serialized class does not. (Monobehaviours do as well) This can be INCREDIBLY useful, it allows one to make a base savedata class and automatically create arrays of serializable classes that are children of savedata
     
  16. DonLoquacious

    DonLoquacious

    Joined:
    Feb 24, 2013
    Posts:
    1,667
    Normal serializable classes support polymorphism just fine until it comes time to save the data- the limitation you're referring to has to do with Unity's built-in serialization. ScriptableObjects manage to maintain their derived types when saved in base-type collections because they exist as assets- unlike typical System.Objects, the collection is not where they "live", it's just where they're "referenced". This limitation is with the Unity editor, and it ceases to be relevant in builds when the editor no longer exists and you have to handle serializing and deserializing objects yourself. At that point, ScriptableObjects have no advantage over normal System.Object types, polymorphic or otherwise.

    For the record though, ZeroFormatter, MsgPack, and most other popular serialization tools support polymorphism just fine. Since ScriptableObjects are no different than any other System.Object type for mutable runtime persistent data, you can use either at that point with equal success.

    My point is not to downplay the usefulness of ScriptableObjects (I use them constantly), but to point out that there's little reason to go out of your way to use them when you only need runtime data persistence, as that's not an area where they have any added benefit over anything else.
     
    Last edited: Jan 18, 2018
  17. Suddoha

    Suddoha

    Joined:
    Nov 9, 2013
    Posts:
    2,824
    That's absolutely true, though the part you quoted was written in a different context.

    He was rather referring to the fact that, no matter if SO or a normal serialized type (class/struct), any changes made during runtime of the build will not be "saved" (serialized back to the asset's data).
    If that was easily do-able, you could actually mess up all the assets. I'd never attempt to workaround that.

    So that's the point where your own (or an existing) solution comes into play...