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 [Resolved] save system help needed

Discussion in 'Scripting' started by BluetheFox, Aug 29, 2023.

  1. BluetheFox

    BluetheFox

    Joined:
    Sep 11, 2016
    Posts:
    14
    Hello. I am currently working on a unity addon that would allow the creation of myst-like games in the editor with relative ease, and currently, I am working on a way to save/load the game and an inventory.

    my inventory items are scriptable objects defined like this:
    Code (CSharp):
    1. using UnityEngine;
    2. using UnityEngine.Events;
    3.  
    4. [CreateAssetMenu(fileName = "InventoryItem", menuName = "CAGE/Inventory Item", order = 1)]
    5. public class InventoryItem : ScriptableObject
    6. {
    7.     public UnityEvent ActionToDoWhenUsed;
    8.     public Texture2D ItemCursor;
    9.     public Texture2D ItemIcon;
    10.     public string ItemName;
    11.     public CageVar VariableWhenHeld;
    12.     public CageVar VariableWhenInInventory;
    13. }

    CageVar is a much simpler class that consists of 2 strings, and I already know how to serialize and save those.


    so, with all that said, in my save system i have successfully been able to "break down" things into the basic bool, int, float, etc. so i can save them properly in a binary file.

    where i am stuck, and what i am asking about, is how do i break things like Texture2d and UnityEvent down into types that i can serialize and save in binary?

    i essentially need to save just enough info to correctly re-construct which inventory item the player is currently holding, and in the near-future, which items are in their inventory list. i cant show the code for the inventory because i haven't actually made it yet, only the player's currently equipped item, which is stored as a "currently equipped item" variable in another script.


    TLDR:i need to save texture2d references and unityevents to my binary save file somehow but idk how
     
  2. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    36,563
    Load/Save steps:

    https://forum.unity.com/threads/save-system-questions.930366/#post-6087384

    An excellent discussion of loading/saving in Unity3D by Xarbrough:

    https://forum.unity.com/threads/save-system.1232301/#post-7872586

    Loading/Saving ScriptableObjects by a proxy identifier such as name:

    https://forum.unity.com/threads/use...lds-in-editor-and-build.1327059/#post-8394573

    When loading, you can never re-create a MonoBehaviour or ScriptableObject instance directly from JSON. The reason is they are hybrid C# and native engine objects, and when the JSON package calls
    new
    to make one, it cannot make the native engine portion of the object.

    Instead you must first create the MonoBehaviour using AddComponent<T>() on a GameObject instance, or use ScriptableObject.CreateInstance<T>() to make your SO, then use the appropriate JSON "populate object" call to fill in its public fields.

    If you want to use PlayerPrefs to save your game, it's always better to use a JSON-based wrapper such as this one I forked from a fellow named Brett M Johnson on github:

    https://gist.github.com/kurtdekker/7db0500da01c3eb2a7ac8040198ce7f6

    Do not use the binary formatter/serializer: it is insecure, it cannot be made secure, and it makes debugging very difficult, plus it actually will NOT prevent people from modifying your save data on their computers.

    https://docs.microsoft.com/en-us/dotnet/standard/serialization/binaryformatter-security-guide
     
  3. BluetheFox

    BluetheFox

    Joined:
    Sep 11, 2016
    Posts:
    14
    thank you kurt; but i already was aware of most of this. the 3rd link to your post about serializing in Json would work for me if not for the requirement that all the sriptableobjects have unique names; and also requiring an additional package from unity's package manager.

    the reason that wont work is because I am developing an addon for unity itself, for other people to use, and I cant really guarantee that they name all their inventory items uniquely, plus I am not sure how to force unity to download a specific package from c#...i could just put it in the documentation as a requirement I suppose.
     
  4. CodeSmile

    CodeSmile

    Joined:
    Apr 10, 2014
    Posts:
    3,899
    You can save textures as an image or byte array and reload them the same way. But if said Texture2D exists as an asset file in the project, you wouldn‘t actually save the texture data. Instead, use the AssetDatabase to get either the asset‘s GUID or path and save that - unless you need to do so at runtime.

    But I wonder: even if these Texture2D get assigned to the SO at runtime, they would still need to be an asset in the project or already loaded from an image. In the latter case you‘d store the path to that image. In the former case, the Texture2D reference is known somehow even at runtime because the user needs to be able to select a specific texture. Therefore I would assume you can map them to indexes for example, and only need to save the index.

    I sort of get the feeling that you may be overcomplicating this. You definitely haven‘t shared all the necessary details, specifically what the binary file is for and whether this save file is runtime only and how these Texture references get assigned.
     
    ZO5KmUG6R likes this.
  5. spiney199

    spiney199

    Joined:
    Feb 11, 2021
    Posts:
    5,769
    Very unlikely you can serialise out a UnityEvent, particularly if it contains Unity object references, as those cannot be serialised out or deserialised.

    Generally the best way to approach with save data is to make is 100% non-Unity specific. Just basic integral types, usually.

    As mentioned above you can serialise a texture above as a byte array, but you probably want to rethink your approach nonetheless.

    For inventories, you generally need each item to express a unique ID, and have a lookup table of these items from their ID. When you save, you just write out the ID's. On load, you look up the items from this table.
     
  6. BluetheFox

    BluetheFox

    Joined:
    Sep 11, 2016
    Posts:
    14

    I didn't share because I didn't realize those things were necessary pieces of info for what my question was. the binary file is literally the runtime-only save file; and these references are set by the developer in the inspector(not me, the person using the addon I'm developing. though for now it is me.)

    excellent answer. i will rethink my approach to what info to save then.

    as for having a lookup table, I'll have to brainstorm a way to implement that for the end-user/developer to be able to easily compile and edit without needing to write a bunch of their own code, since I expect them to make their own inventory items for the game they are working on.
     
    Last edited: Aug 30, 2023
  7. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    36,563
    Given what you are attempting, something for the general user to succeed with making a Myst game, you will almost certainly need to use some type of string- or numeric-based lookup for the assets. Here's why:

    If it was a small collection of data you could make a workflow where the user must add each asset into your own directory object, like a Scriptable Object, and then you can load those before the game.

    The problem in practice will be that if you make reference to these items in any scene or prefab that gets loaded, 100% of everything gets loaded into memory at once. That might be fine on super-high-end systems, even if it's a bit piggish memory-wise, but it also might blow out memory on lower end systems or mobile systems.

    This is where either Resources.Load<T>() or the much-newer-fangled Addressables system comes into play, as that lets you lazy-load these only on demand.

    If you want the simplest system, make your own code to move the player's assets into a
    Resources/
    located folder, then make an index of those names yourself, ensuring uniqueness. Then you know they can be loaded.

    If you are tempted to put unique identifiers of your own on each item, that's definitely the knee-jerk computer science way to do things, but in practice with how stuff is serialized, moved, source controlled, fiddled and diddled, that just becomes a horribly fragile system on top of the filesystem, which is already robust.

    Even if you think you're clever and hold the user's hand through all that lookup and tracking, there WILL be bugs, and the user WILL break your system, and then the bugs will be very hard to reason about because your identifier might go missing, get corrupted or be repeated (eg, not unique). That's why I always load stuff from Resources.Load<T>() with guaranteed-unique names, enforced unique because all related items are in one subfolder.

    Otherwise I have found the Addressables system to be massively overcomplicated for what it does above the pure simple built-in Resources.Load<T>() system, which is all that I ever use. I know others here disagree, but I'm just telling you my experience.
     
  8. BluetheFox

    BluetheFox

    Joined:
    Sep 11, 2016
    Posts:
    14
    so if I cant use unique identifiers because apparently "its fragile" and cant use resources.load because "the user likely wont listen to instructions on where to store assets" then what do you suggest I do? ideally I would do both of these if not for these issues.

    like; store things in the resources folder and then use their unique identifiers to look them up with a for loop that checks "is this the one with this specific identifier? no? how about this next one?"

    what I'm wondering is how true it actually is that unique identifiers will break with git and stuff. if its a property on the monobehavior script like any other string property I don't really think it would. that still leaves the resources folder problem though
     
  9. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    36,563
    That is if your user is even USING source control. Don't count on it. Most people fly without it and then end up in here complaining that they broke their project and can't fix it. :)

    You can get a demo of exactly how easy it is to get tangled up.

    Make a ScriptableObject with a field. Put some data in that field. Exit Unity and commit.

    Now open Unity and change that field.

    Go back to source control. No changes! That's because Unity actually fails to flush changes to disk.

    Unity WILL those changes to disk when you exit, so it has nothing at all to do with "Saving," as the change is already done, but simply not shown on disk.

    We fight this ALL THE TIME in a team environment. It's infuriating that Unity doesn't
    fflush()
    the #@$@#$ file handle, but they haven't until now, it's likely not gonna change.

    This is just one way that stuff gets out of sync: it gets committed unchanged, then confusion begins.
     
  10. BluetheFox

    BluetheFox

    Joined:
    Sep 11, 2016
    Posts:
    14
    well if it's incompetent programming on Unity's part and most people wont be using source control anyways, then I don't see a reason to care.

    anyways, the system I will go with is using the unique identifiers to search through a "list of inventory items" since that is the only real solution according to both you and spiney's suggestions.

    the only thing now is how to make it easy for the user to build that list so that their game "just works", ideally, without additional programming required on their part or having to nag them about keeping stuff in a specific folder.


    my idea is to have a save system attached to a game object with the understanding that they can't save without a save system, and a menu item to easily add the save system to the scene. and have that save system execute code in the editor to build it's own list of inventory items based on what is referenced in key parts of the scene; and have it save/load only the unique identifier in the runtime. when the game loads, it will look up the identifier in the list. should be simple enough to do.
     
    Last edited: Aug 30, 2023
  11. dlorre

    dlorre

    Joined:
    Apr 12, 2020
    Posts:
    700
    You can use base 64 to encode a texture: https://en.wikipedia.org/wiki/Base64

    As for the event you can encode its name and then lookup in a dictionary. Otherwise you can look in the meta file and do something similar I guess.
     
  12. BluetheFox

    BluetheFox

    Joined:
    Sep 11, 2016
    Posts:
    14


    I have now finished the entire system based on everyone else's suggestions. turns out I didn't need to actually save every part of the inventory items, including the textures- all I needed to do was save a unique identifier and then look them up based on that identifier upon loading.

    i ended up not even needing a list of inventory items in my case; I instead just borrow from references to them throughout the scene (which works because the way things are set up, the player would never have an inventory item without it being referenced somewhere in the scene anyways).

    I'm not sure how to condense my code down into something more minimal to post for future readers unfortunately... but the basic idea at least is:

    saving:
    1. every inventory item scriptable object has a unique identifier property that the user is told to assign if its missing at key points during the save process
    2. we iterate through every inventory item and save their identifiers, and only their identifiers

    loading:
    1. we load up all the saved identifiers
    2. we iterate through every object in the scene that references inventory items and check if the inventory item's id matches any of the identifiers we just loaded, and if so, we put that inventory item in the player's inventory again- affectively restoring the game's state to how it was when they saved
     
    dlorre likes this.
  13. spiney199

    spiney199

    Joined:
    Feb 11, 2021
    Posts:
    5,769
    Don't really get Kurt's concern about unique ID's. This alone is effective enough:
    Code (CSharp):
    1. [SerializeField, HideInInspector]
    2. private string _guid = System.Guid.NewGuid().ToString();
    And shouldn't change unless you Right Click -> Reset the SO.

    File names change constantly so I'd never personally use them as an ID. I wouldn't want to break users' saves because I deigned to rename a file.

    This is not at all consistent with my experience and feels completely wrong, honestly. I can change a field, CRTL-S to save, and any changes show in my source control.

    Unity writes changes in dirty assets to disk when you save. It's always worked that way for me.