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

Question Save system questions

Discussion in 'Scripting' started by MACMAN2003, Jul 13, 2020.

  1. MACMAN2003

    MACMAN2003

    Joined:
    Oct 27, 2019
    Posts:
    12
    I am wondering how to implement a save system into the game I am making. Every tutorial I have looked at has used simple scripts and scenes to show off their version of a save system and not a single one I have watched has given me even a hint as to where to start.

    My scenes are very complex (e.g. multiple "enemy" prefabs with multiple scripts attached to each) and all the tutorials I watched (I might be receiving the wrong message from them) have given me the impression that i would have to make a save script with every iteration of each var/int/bool/float/vector3 for every object in the scene (e.g. enemy1 has 40 health and enemy2 has 10 health, and I would have to make the save script contain enemy1's health float as well as enemy2's health float)

    In other words, it seems like for every scene I would need a specific scene save script that has all the variables, integers, floats, booleans, and vector3's of every separate object in that scene. And if I wanted to change something (like place down another enemy prefab and make an enemy3) I would have to redo the save script to account for the changes made in the scene.

    Do I put a single master "save script" for each scene, or do I put a "save script" on everything in the scene?

    Am I missing something?
     
  2. Cyber-Dog

    Cyber-Dog

    Joined:
    Sep 12, 2018
    Posts:
    352
    This is rather interesting, I have never attempted a save/load setup nor read up on one.

    If I was to have a crack at it myself, this is where I would start. Note, this code is theory/sudo. You would have to work with it to get something going.

    The idea being every game object you want to save data on requires a SaveItem component, and you then use reflection to gather all the public variables available and choose which ones you want to store. I did not write that part as its a whole different piece of work for the editor script... Then in a scene, you have a save item manager that you can call so save/load.

    The way this is written is open to flaws. It doesn't deal with parent transforms, for instantiation. But its something to get your mind thinking :)

    Code (CSharp):
    1.  
    2. [System.Serializable]
    3. public class SaveItemDetail
    4. {
    5.     FieldInfo info;
    6.     Object value;
    7.     Component targetComponent;  
    8. }
    9.  
    10. public class SaveItem : MonoBehaviour
    11. {
    12.     private string gameObjectName;
    13.    
    14.     public List<SaveItemDetail> saveItemDetails;
    15.    
    16.     public void SavePrefs()
    17.     {
    18.         foreach(SaveItemDetail item in saveItemDetails)
    19.         {
    20.             item.value = info.GetValue(targetComponent);
    21.         }
    22.     }
    23.    
    24.     public void LoadPrefs()
    25.     {
    26.         foreach(SaveItemDetail item in saveItemDetails)
    27.         {
    28.             if (this.GetComponent<targetComponent.GetType()>() is null)
    29.                 targetCompoent = this.AddCompoent<targetComponent.GetType()>()
    30.             else
    31.             item.SetValue(targetComponent, item.value);
    32.         }
    33.     }
    34.    
    35.     void Reset(){
    36.         gameObjectName = this.GameObject.name;
    37.     }
    38. }
    39.  
    40. public class SaveItemEditor : Editor
    41. {
    42.     // Create your own editor view to handle editing the array list, where you use reflection to grab public valraibles.
    43. }
    44.  
    45. public class SaveItemManager
    46. {
    47.     private const string savePath = "....";
    48.    
    49.     public void LoadSaves()
    50.     {
    51.         List<SaveItem> saveItems = //Deserialize from save path.
    52.         foreach(SaveItem item in saveItems)
    53.         {
    54.             GameObject go = (GameObject.Find(item.gameObjectName) is null) ? Instantiate(new Gamoebject(item.gameObjectName)) : GameObject.Find(item.gameObjectName);
    55.             if (go.GetComponent<SaveItem>() is null)
    56.                 go.AddComponent<SaveItem>() = item;
    57.             else
    58.                 go.GetComponent<SaveItem>() = item;
    59.                
    60.             item.LoadPrefs();
    61.         }
    62.     }
    63.    
    64.     public void StoreSaves()
    65.     {
    66.         List<SaveItem> saveItems = GameObject.FindObjectsOfType<SaveItem>().ToList();
    67.         // Serailes class to SavePath
    68.     }
    69. }
     
  3. MACMAN2003

    MACMAN2003

    Joined:
    Oct 27, 2019
    Posts:
    12
    I should have mentioned that I am entirely new to game making, so what may be "complex" to me is "simple" to veteran game-developers.

    If it helps, an example of a scene (Using words, I don't know how to screenshot)
    Level environment (A bunch of small and large meshes fit together like small hallway meshes and large special room meshes)

    Load zones, a static area that acts like an airlock. (side1 is active and side2 is not, when the load zone is triggered, the sides swap activity so side2 is active and side1 is inactive).

    Enemies, with health, AI, target, drops, and damage scripts. Prefabs are attached to these that tell what each enemy drops.

    Weapons, with ammo, clip, and a script that puts a prefab in the player's inventory. Prefabs are attached to these that tell what weapon is put in the player's inventory.

    Lockers, with an open/close script.

    Breakable loot, with health, and drop scripts. Prefabs are attached to tell what they drop when broken.

    The Player, with health, armor, movement, looking, keys, interact, and weapons scripts. Many prefabs are attached to the player's child objects (like a weapon master with three "weapon holder" objects attached, and prefabs are attached to the weapon holders and deleted when weapons are dropped.)

    Held weapons, with damage, clip, ammo, drop, and bobbing scripts (drop script stores a prefab weapon drop)

    A Canvas, with things like ammo, and health. (UI is updated by the scripts on the player)

    Doors, with an open/close script.

    Keys, with a script that adds a key to the players inventory.

    Pickups, with health, armor, or ammo scripts attached.

    Dropped weapons, that are the same as regular weapons but their clip is changed to be the same amount as a held weapon's was when it was dropped.

    Events, (e.g. open a door and an animation plays where a enemy mesh is "killed" by a falling ceiling tile).

    With all the things inside the scene it seems astronomically easier to just save the state of the game, rather than pick and choose what I want saved.
     
  4. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    38,514
    Saving/Loading is never simple.

    There's tons of tutorials out there on Youtube to give you an idea of the process.

    Basically step 1 is identify what needs to be saved. Might just be a single digit, as in "what level am I on?" Might be a lot more: GameObjects, inventories, internal states of running scripts, positions, rotations, etc.

    Keep in mind the more you save the more debugging and painstaking engineering you're going to need to do. And it gets really hairy.

    Step 2 - design a data structure to hold all the above data, as well as enough extra data to restore it on LOAD (i.e., a foolproof way of moving the data back to whoever originally asked for it to be saved).

    Step 3 - design and implement something to iterate the stuff you want to save and extract that into the data structure

    Step 4 - easy : write it to disk (generally use JSON for serialization)

    On load, it's the opposite:

    Step 1 - read structure from disk into memory

    Step 2 - iterate all data, reinject it into the running game. This might take multiple steps: iterate some data to figure out what scene(s) to load, then load that scene(s), then iterate the rest of the data to inject it into the running scene, possibly even creating additional GameObjects in the scene based on the save data.

    Step 3 - let the game run!

    Let me just add that with ALL such massive engineering efforts, start small. Savegame system version 1 should just save and restore your score, or something equally trivial.

    But get that working and prove out ALL steps from extracting, storing, writing, reading, restoring, restarting, and do so with very simple data, and only then start to add the next piece, such as your gold or or what scene you are on, or whatever is next to save.
     
    Last edited: Oct 29, 2021
    NyamaNyama, DTECTOR, Canley and 7 others like this.
  5. MACMAN2003

    MACMAN2003

    Joined:
    Oct 27, 2019
    Posts:
    12
    The tutorials on youtube make it seem like in order to save, I have to reference every var, int, float, bool, string, vector3, quaternion, and component on every unique object, light, rigidbody, and empty in the scene to put into the save script.
     
  6. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    38,514
    My understanding of computers work and how data resides in memory would agree with this. There are constructs to help you accomplish this, but these constructs require you to use them in your engineered solution.
     
  7. MACMAN2003

    MACMAN2003

    Joined:
    Oct 27, 2019
    Posts:
    12
    Any hint as to what these constructs are?
     
  8. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    38,514
    Reflection can help you locate unspecified future variables, but you still have to make sense of any stored serialized data, otherwise how would you get it back into those variables?

    Think of this: you build something out of Lego and you photograph it.

    Is that enough to rebuild it?

    It probably is if that thing is 2 bricks clipped together.

    It probably isn't if that thing is 100,000 specialized bricks all hidden from one another.

    You might need a lot of photographs.

    That's why I said try to keep the saved data small. Otherwise you'll instantly be bogged down.
     
  9. bobisgod234

    bobisgod234

    Joined:
    Nov 15, 2016
    Posts:
    1,042
    You don't need to save every single variable in your game, only the ones that need to be saved.

    For example, you don't need to store the settings of your directional light if it is static and unchanging in the scene. You probably do need to save your player's position though.

    You often don't need to save too much data, depending on the game. For example, you can generally get away with not saving anything related to your enemy AI, if your AI is going to be immediately aggro'd anyway on load due to being near the player.

    If I were to implement a save system, I would probably start with something like this:

    Code (CSharp):
    1. public interface ISaveable
    2. {
    3.   public Dictionary<string, string> GetSaveData();
    4.   public void SetSaveData(Dictionary<string, string>);
    5. }
    and implement it on every single entity that needs to be saved (player, enemies, but not lights, level geometry).

    Code (CSharp):
    1. public class Player : Monobehaviour, ISaveable
    2. {
    3.   public Dictionary<string, string> GetSaveData()
    4.   {
    5.     return new Dictionary<string, string>(){
    6.       {"X", transform.position.x},
    7.       {"Y", transform.position.y},
    8.     }
    9.   }
    10.  
    11.   public void SetSaveData(Dictionary<string, string> saveData)
    12.   {
    13.     Vector2 pos = new Vector2(saveData["X"], saveData["Y"]);
    14.     transform.position = pos;
    15.   }
    16. }
     
  10. PraetorBlue

    PraetorBlue

    Joined:
    Dec 13, 2012
    Posts:
    7,888
    It really depends on your game. But there's a reason Resident Evil and Final Fantasy had save points instead of letting players save at any time. Think carefully about your game and what (and when) you actually need to save. Think also about the possibility of limiting the player's opportunities to save. Most modern games I've seen do some kind of autosaving, and they typically do that autosaving at or around checkpoints. For example The Last of Us 2. There's plenty of scenes where the player is fighting a dozen different zombies. But the game never saves the health of any zombies in its save files. It pretty much just saves the player's inventory and what checkpoint they're up to.

    For example, you talked about saving every single enemy's health and location and rotation etc.. Maybe reconsider allowing the player to save "in the action" Maybe the player can only save between battles? Just something to think about. The other thing is, you don't need to save every variable in the scene. Maybe you need to store the enemy's current health, but something like the enemy's maximum health is simply part of the static data for your game. It's on the enemy prefab. You just need to know which prefab to use for the enemy.
     
    SeerSucker69 and Kurt-Dekker like this.
  11. MACMAN2003

    MACMAN2003

    Joined:
    Oct 27, 2019
    Posts:
    12
    Using your example, if I have two different objects (player, enemy) and I wanted to save the second object's position, would I have to make another "ISaveable" file? If I did, wouldn't that make two different save files?

    And in code, would bools/floats/ints/vars be referred to as "strings" or would I also have to add
    Code (CSharp):
    1. public Dictionary<string, string, bool, float, var, int>(){
    2. {"X", transform.position.x},
    3. {"Y", transform.position.y},
    4. {"F", float},
    5. {"B", bool},
    6. {"I", int},
    7. {"V", var},
    8. }
    to it?
     
  12. bobisgod234

    bobisgod234

    Joined:
    Nov 15, 2016
    Posts:
    1,042
    Both your player and enemy script can implement the same ISaveable interface.

    Code (CSharp):
    1. public class Player : Monobehaviour, ISaveable
    2. {
    3. }
    4.  
    5. public class Enemy: Monobehaviour, ISaveable
    6. {
    7. }
    All the interface will do is say that the class has a Save/Load function, How exactly that works is dependent on the object's specific implementation of that function, but presumably you already have a player and enemy script, so just put that unique logic in those classes.

    As for your second question, the example I gave maps strings to strings. If you want to save floats, ints, bools etc first convert them to a string

    Code (CSharp):
    1. enemHealth.ToString()
    And when you load them back in, convert them from a string

    Code (CSharp):
    1. enemyHealth = float.parse(data["Health"]);
    Here is an example based on my first post (I just realized I didn't do any of the converting necessary in that example)

    Code (CSharp):
    1. public class Player : Monobehaviour, ISaveable
    2. {
    3.   public Dictionary<string, string> GetSaveData()
    4.   {
    5.     return new Dictionary<string, string>(){
    6.       {"X", transform.position.x.ToString()},
    7.       {"Y", transform.position.y.ToString()},
    8.       {"Health", myHealth.ToString()},
    9.       {"Ammo", myAmmo.ToString()},
    10.     }
    11.   }
    12.  
    13.   public void SetSaveData(Dictionary<string, string> saveData)
    14.   {
    15.     Vector2 pos = new Vector2(float.parse(saveData["X"]), float.parse(saveData["Y"]));
    16.     transform.position = pos;
    17.     myHealth = float.parse(saveData["Health"]);
    18.     myAmmo = int.parse(saveData["Ammo"]);
    19.   }
    20. }
     
    Last edited: Jul 16, 2020
    Kurt-Dekker and MACMAN2003 like this.
  13. MACMAN2003

    MACMAN2003

    Joined:
    Oct 27, 2019
    Posts:
    12
    If I put down another "enemy" and give it the exact same script that I gave to the first enemy, will it work with both, or will I have to specify that they are completely separate objects for it to work?

    Am I also correct in thinking that I will have to put
    Code (CSharp):
    1. [System.Serializable]
    into the code?
     
  14. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    38,514
    However you choose to route restored save data back to the objects requiring it, it must be completely unambiguous.
     
  15. bobisgod234

    bobisgod234

    Joined:
    Nov 15, 2016
    Posts:
    1,042
    You may need to save an object name or some sort of other ID, so when you go to reload the game, you know what saved data goes with which object.

    No, the example code has nothing to do with Unity's serialisation.
     
  16. MACMAN2003

    MACMAN2003

    Joined:
    Oct 27, 2019
    Posts:
    12
    After putting down the second enemy and renaming it to "enemy2" in the inspector and telling the code to save the object name too, does it successfully recognize them as different objects when I save?
     
  17. bobisgod234

    bobisgod234

    Joined:
    Nov 15, 2016
    Posts:
    1,042
    No, nothing I wrote does that, your supposed to do that bit yourself.

    When you go to save everything, you are probably going to be iterating over each ISaveable and getting their save data. When you do that, also get the objects name and save that too.
     
  18. PraetorBlue

    PraetorBlue

    Joined:
    Dec 13, 2012
    Posts:
    7,888
    It's possible, I actually just thought the tmpro inputfield was a regular inputfield but with a tmpro label instead of default text.
     
  19. MACMAN2003

    MACMAN2003

    Joined:
    Oct 27, 2019
    Posts:
    12
    I have been thinking it over and I think that the best way for me to implement saving is to make a code that checks for everything in the scene (gameobjects, components, vars/ints/floats/etc) and then do the complicated json/binary writing.

    Thank you all so much for helping me!
    (sorry for necro-posting)
     
    Kurt-Dekker and Cyber-Dog like this.