Forum begins after the advertisement:


[General] Save/Load System

Viewing 10 posts - 1 through 10 (of 10 total)
  • Author
    Posts
  • #16645
    Cam
    Level 22
    Silver Supporter (Patron)
    Helpful?
    Up
    1
    ::

    Hi all I’ve been ask to show my save and load system —so ill do my best to explain what I’ve done. before we get in to it ill show a quick video of how I’ve changed my game from the tutorial and then ill get to showing the save and load system

    View post on imgur.com

    so as you see in my video I need the save data to store my Runes, Potion, Passive Upgrades and I also have it save what every weapon is equipped and the players skins the weapons save as so when you get back to the base scene or come back to playing the game you last weapon used is still equipped and in the future it will enable to save what weapons you have unlocked, and the character save is I have build a way to have custom skins that every player can unlock and change what they look like. also a change I made was that instead of change the character you can only play as one character but you change you starting weapon instead witch made the weapon save important to my game.

    SaveLoadManager.cs

    using System.Collections.Generic;
    using System.IO;
    using UnityEngine;
    using UnityEngine.SceneManagement;
    using System.Linq;
    
    
    public class SaveLoadManager : MonoBehaviour
    {
        private string runeBagsSavePath;
        private string equippedWeaponSavePath;
        private string toggleStateSavePath;
        private string passiveUpgradesSavePath;
        private string potionBagSavePath;
        private string equippedPotionSavePath;
    
        private const string UpgradesKey = "Upgrades";
        private const string OneOffItemsKey = "OneOffItems";
    
        private void Awake()
        {
            string saveFolderPath = Path.Combine(Application.persistentDataPath, "Save");
    
            if (!Directory.Exists(saveFolderPath))
            {
                Directory.CreateDirectory(saveFolderPath);
            }
    
            runeBagsSavePath = Path.Combine(saveFolderPath, "runeBags.json");
            equippedWeaponSavePath = Path.Combine(saveFolderPath, "equippedWeapon.json");
            toggleStateSavePath = Path.Combine(saveFolderPath, "toggleState.json");
            passiveUpgradesSavePath = Path.Combine(saveFolderPath, "passiveUpgrades.json");
            potionBagSavePath = Path.Combine(saveFolderPath, "potionBag.json");
            equippedPotionSavePath = Path.Combine(saveFolderPath, "equippedPotion.json");
    
            SceneManager.sceneLoaded += OnSceneLoaded;
            SceneManager.sceneUnloaded += OnSceneUnloaded;
    
            Time.timeScale = 1f;
        }
    
        private void OnDestroy()
        {
            SceneManager.sceneLoaded -= OnSceneLoaded;
            SceneManager.sceneUnloaded -= OnSceneUnloaded;
        }
    
        public void SaveRuneBags(RuneBagSerializable runeBag, RuneBagSerializable equippedRuneBag)
        {
            RuneBagsData data = new RuneBagsData
            {
                runeBag = runeBag,
                equippedRuneBag = equippedRuneBag
            };
    
            string json = JsonUtility.ToJson(data, true);
            string encryptedJson = EncryptionUtility.Encrypt(json);
            File.WriteAllText(runeBagsSavePath, encryptedJson);
        }
    
       public void SaveUpgrades(List<PassiveUpgrades.Upgrade> upgrades, List<PassiveUpgrades.OneOffItem> oneOffItems, bool gameSceneLoaded)
        {
            PassiveUpgradesData data = new PassiveUpgradesData
            {
                upgrades = upgrades,
                oneOffItems = oneOffItems,
                gameSceneLoaded = gameSceneLoaded
            };
    
            string json = JsonUtility.ToJson(data, true);
            string encryptedJson = EncryptionUtility.Encrypt(json);
    
            // Save the encrypted JSON to the file
            File.WriteAllText(passiveUpgradesSavePath, encryptedJson);
        }
    
        public (List<PassiveUpgrades.Upgrade>, List<PassiveUpgrades.OneOffItem>, bool) LoadUpgrades()
        {
            if (File.Exists(passiveUpgradesSavePath))
            {
                string encryptedJson = File.ReadAllText(passiveUpgradesSavePath);
    
                // Decrypt the JSON
                string json = EncryptionUtility.Decrypt(encryptedJson);
    
                PassiveUpgradesData data = JsonUtility.FromJson<PassiveUpgradesData>(json);
                return (data.upgrades, data.oneOffItems, data.gameSceneLoaded);
            }
            else
            {
                Debug.LogWarning("Passive upgrades save file not found.");
                return (new List<PassiveUpgrades.Upgrade>(), new List<PassiveUpgrades.OneOffItem>(), false);
            }
        }
    
        public void LoadRuneBags(RuneInventory runeInventory)
        {
            if (File.Exists(runeBagsSavePath))
            {
                string encryptedJson = File.ReadAllText(runeBagsSavePath);
                string json = EncryptionUtility.Decrypt(encryptedJson);
                RuneBagsData data = JsonUtility.FromJson<RuneBagsData>(json);
                runeInventory.runeBag = data.runeBag;
                runeInventory.equippedRuneBag = data.equippedRuneBag;
            }
            else
            {
                Debug.LogWarning("Save file not found at " + runeBagsSavePath);
            }
        }
    
        public void SaveEquippedWeapon(WeaponData equippedWeapon)
        {
            EquippedWeaponData data = new EquippedWeaponData
            {
                equippedWeapon = equippedWeapon
            };
    
            string json = JsonUtility.ToJson(data, true);
            File.WriteAllText(equippedWeaponSavePath, json);
        }
    
        public WeaponData LoadEquippedWeapon()
        {
            if (File.Exists(equippedWeaponSavePath))
            {
                string json = File.ReadAllText(equippedWeaponSavePath);
                EquippedWeaponData data = JsonUtility.FromJson<EquippedWeaponData>(json);
                return data.equippedWeapon;
            }
            else
            {
                Debug.LogWarning("Equipped weapon save file not found.");
                return null;
            }
        }
    
        public void SaveToggleState(bool isOn)
        {
            ToggleStateData data = new ToggleStateData
            {
                isOn = isOn
            };
    
            string json = JsonUtility.ToJson(data, true);
            File.WriteAllText(toggleStateSavePath, json);
        }
    
        public bool LoadToggleState()
        {
            if (File.Exists(toggleStateSavePath))
            {
                string json = File.ReadAllText(toggleStateSavePath);
                ToggleStateData data = JsonUtility.FromJson<ToggleStateData>(json);
                return data.isOn;
            }
            else
            {
                Debug.LogWarning("Toggle state save file not found.");
                return false;
            }
        }
    
        public void SavePotionInventory(PotionInventory potionInventory)
        {
            PotionBagSerializable data = new PotionBagSerializable
            {
                potions = potionInventory.potionBag.potions, // Already storing potion names
                equippedPotion = potionInventory.equippedPotion?.potionName
            };
    
            string json = JsonUtility.ToJson(data, true);
            File.WriteAllText(potionBagSavePath, json);
    
            Debug.Log("Potion inventory saved: " + json);
        }
    
        public PotionBagSerializable LoadPotionInventory()
        {
            if (File.Exists(potionBagSavePath))
            {
                string json = File.ReadAllText(potionBagSavePath);
                PotionBagSerializable data = JsonUtility.FromJson<PotionBagSerializable>(json);
    
                Debug.Log("Potion inventory loaded: " + json);
                return data;
            }
            else
            {
                Debug.LogWarning("Potion inventory save file not found.");
                return null;
            }
        }
    
        private void OnSceneLoaded(Scene scene, LoadSceneMode mode)
        {
            RuneInventory runeInventory = FindObjectOfType<RuneInventory>();
            if (runeInventory != null)
            {
                LoadRuneBags(runeInventory);
            }
    
            ToggleGameObjects toggleGameObjects = FindObjectOfType<ToggleGameObjects>();
            if (toggleGameObjects != null)
            {
                bool toggleState = LoadToggleState();
                toggleGameObjects.SetToggleState(toggleState);
            }
    
            PassiveUpgrades passiveUpgrades = FindObjectOfType<PassiveUpgrades>();
            if (passiveUpgrades != null)
            {
                var (upgrades, oneOffItems, gameSceneLoaded) = LoadUpgrades();
                passiveUpgrades.purchasedUpgrades = upgrades;
                passiveUpgrades.purchasedOneOffItems = oneOffItems;
                passiveUpgrades.SetGameSceneLoaded(gameSceneLoaded);
            }
    
            PotionInventory potionInventory = FindObjectOfType<PotionInventory>();
            if (potionInventory != null)
            {
                PotionBagSerializable loadedData = LoadPotionInventory(); // Load the serialized data
    
                // Convert potion names back to PotionData objects
                potionInventory.potionBag.potions = loadedData.potions
                    .Select(potionName => potionInventory.allPotions.Find(p => p.potionName == potionName))
                    .Where(potion => potion != null) // Ensure only valid potions are added
                    .Select(potion => potion.potionName) // Convert back to a list of names (string)
                    .ToList();
    
                // Convert the equipped potion name back to a PotionData object
                potionInventory.equippedPotion = potionInventory.allPotions
                    .Find(p => p.potionName == loadedData.equippedPotion);
    
                if (potionInventory.equippedPotion == null && !string.IsNullOrEmpty(loadedData.equippedPotion))
                {
                    Debug.LogWarning($"Equipped potion '{loadedData.equippedPotion}' not found in allPotions.");
                }
    
                potionInventory.UpdateEquippedPotionDisplay();
            }
        }
    
        private void OnSceneUnloaded(Scene scene)
        {
            RuneInventory runeInventory = FindObjectOfType<RuneInventory>();
            if (runeInventory != null)
            {
                SaveRuneBags(runeInventory.runeBag, runeInventory.equippedRuneBag);
            }
    
            PassiveUpgrades passiveUpgrades = FindObjectOfType<PassiveUpgrades>();
            if (passiveUpgrades != null)
            {
                SaveUpgrades(passiveUpgrades.purchasedUpgrades, passiveUpgrades.purchasedOneOffItems, passiveUpgrades.IsGameSceneLoaded());
            }
    
            PotionInventory potionInventory = FindObjectOfType<PotionInventory>();
            if (potionInventory != null)
            {
                SavePotionInventory(potionInventory);
            }
        }
    
        public void DeleteRuneSaveFiles()
        {
            if (File.Exists(runeBagsSavePath))
            {
                File.Delete(runeBagsSavePath);
                Debug.Log("Rune bags save file deleted.");
            }
    
            if (File.Exists(equippedWeaponSavePath))
            {
                File.Delete(equippedWeaponSavePath);
                Debug.Log("Equipped weapon save file deleted.");
            }
    
            if (File.Exists(toggleStateSavePath))
            {
                File.Delete(toggleStateSavePath);
                Debug.Log("Toggle state save file deleted.");
            }
    
            if (File.Exists(passiveUpgradesSavePath))
            {
                File.Delete(passiveUpgradesSavePath);
                Debug.Log("Passive upgrades save file deleted.");
            }
    
            if (File.Exists(potionBagSavePath))
            {
                File.Delete(potionBagSavePath);
                Debug.Log("Potion inventory save file deleted.");
            }
    
            if (File.Exists(equippedPotionSavePath))
            {
                File.Delete(equippedPotionSavePath);
                Debug.Log("Equipped potion save file deleted.");
            }
        }
    
        [System.Serializable]
        private class RuneBagsData
        {
            public RuneBagSerializable runeBag;
            public RuneBagSerializable equippedRuneBag;
        }
    
        [System.Serializable]
        private class EquippedWeaponData
        {
            public WeaponData equippedWeapon;
        }
    
        [System.Serializable]
        private class ToggleStateData
        {
            public bool isOn;
        }
    
        [System.Serializable]
        private class PassiveUpgradesData
        {
            public List<PassiveUpgrades.Upgrade> upgrades;
            public List<PassiveUpgrades.OneOffItem> oneOffItems;
            public bool gameSceneLoaded;
        }
    
        [System.Serializable]
        public class PotionBagData
        {
            public List<PotionData> potions = new List<PotionData>();
            public PotionData equippedPotion; // Changed to single PotionData
        }
    }

    OKKKK lets have a look I’ve split the save files up so I can have a play with them while testing things out

    1. Setting Up Save Paths The SaveLoadManager uses separate JSON files to save different data types. Paths are defined in the Awake method, and directories are created if they don’t exist.

    2. Save and Load Methods For each data type, the manager provides Save and Load methods. These methods serialize data to JSON, optionally encrypt it, and write to or read from the file system.

      public void SaveRuneBags(RuneBagSerializable runeBag, RuneBagSerializable equippedRuneBag)
      {
       RuneBagsData data = new RuneBagsData { runeBag = runeBag, equippedRuneBag = equippedRuneBag };
       string json = JsonUtility.ToJson(data, true);
       string encryptedJson = EncryptionUtility.Encrypt(json);
       File.WriteAllText(runeBagsSavePath, encryptedJson);
      }
      public void LoadRuneBags(RuneInventory runeInventory)
      {
       if (File.Exists(runeBagsSavePath))
       {
           string encryptedJson = File.ReadAllText(runeBagsSavePath);
           string json = EncryptionUtility.Decrypt(encryptedJson);
           RuneBagsData data = JsonUtility.FromJson<RuneBagsData>(json);
           runeInventory.runeBag = data.runeBag;
           runeInventory.equippedRuneBag = data.equippedRuneBag;
       }
       else
       {
           Debug.LogWarning("Save file not found at " + runeBagsSavePath);
       }
      }

      so i call these mothods where and when i want to save and load my runs eg when collecting a run i call the save mothed and when i load a new scene i call the load method so when the game starts or anthing it loads any save data that it need to. i added Encryption so my freind that are play testing it can mode there runes and get on the top of the leader board ill show my Encryption later

    3. Managing Scene Transitions The system hooks into Unity’s scene events to automatically save and load data when a scene changes.

    private void OnSceneLoaded(Scene scene, LoadSceneMode mode)
    {
        RuneInventory runeInventory = FindObjectOfType<RuneInventory>();
        if (runeInventory != null)
        {
            LoadRuneBags(runeInventory);
        }
        // ... Load other data types
    }
    private void OnSceneUnloaded(Scene scene)
    {
        RuneInventory runeInventory = FindObjectOfType<RuneInventory>();
        if (runeInventory != null)
        {
            SaveRuneBags(runeInventory.runeBag, runeInventory.equippedRuneBag);
        }
        // ... Save other data types
    }
    1. Custom Data Classes To facilitate JSON serialization, I created simple classes that map directly to the save data structure:
      [System.Serializable]
      private class RuneBagsData
      {
       public RuneBagSerializable runeBag;
       public RuneBagSerializable equippedRuneBag;
      }
      [System.Serializable]
      private class EquippedWeaponData
      {
       public WeaponData equippedWeapon;
      }

      This structure ensures that the save and load process remains clean and adaptable for new data types.

    then you can go though you game and add where you want to to save and load ill show you my SceneController that deals with changing to a set scene but also Saves my Runes every time it changes

    using UnityEngine;
    using UnityEngine.SceneManagement;
    using UnityEngine.UI;
    using TMPro;
    using System.Collections;
    
    public class SceneController : MonoBehaviour
    {
        private SaveLoadManager saveLoadManager;
        private RuneInventory runeInventory;
    
        public GameObject loadingScreen, loadingIcon;
        public TMP_Text loadingText;
    
        private void Awake()
        {
            saveLoadManager = FindObjectOfType<SaveLoadManager>();
            runeInventory = FindObjectOfType<RuneInventory>();
        }
    
        public void SceneChange(string name)
        {
            if (saveLoadManager != null && runeInventory != null)
            {
                saveLoadManager.SaveRuneBags(runeInventory.runeBag, runeInventory.equippedRuneBag);
            }
            else
            {
                Debug.LogWarning("SaveLoadManager or RuneInventory not found. Cannot save rune bags.");
            }
    
            StartCoroutine(LoadMain(name));
        }
    
        private IEnumerator LoadMain(string name)
        {
            loadingScreen.SetActive(true);
            loadingText.text = "Loading...";
    
            AsyncOperation asyncLoad = SceneManager.LoadSceneAsync(name);
            asyncLoad.allowSceneActivation = false;
    
            while (!asyncLoad.isDone)
            {
                // Update loading icon or text here to show progress
                if (asyncLoad.progress < 0.9f)
                {
                    loadingText.text = $"Loading {asyncLoad.progress * 100}%";
                }
                else
                {
                    loadingText.text = "Press any key to continue";
    
                    // Wait for user input to activate the scene
                    if (Input.anyKeyDown)
                    {
                        asyncLoad.allowSceneActivation = true;
                    }
                }
    
                yield return null;
            }
    
            // Reset time scale just in case it was modified in the previous scene
            Time.timeScale = 1f;
            loadingScreen.SetActive(false);
        }
    }

    added bonce here its my loading screen that show load % and click any key to continue =)

    and last ill show my Encryption EncryptionUtility.cs

    using System;
    using System.IO; // Add this line
    using System.Security.Cryptography; // Add this line
    using System.Text;
    
    public static class EncryptionUtility
    {
        private static readonly string key = ""; // Replace with your key
        private static readonly string iv = ""; // Replace with your IV
    
        public static string Encrypt(string plainText)
        {
            using (Aes aes = Aes.Create())
            {
                aes.Key = Convert.FromBase64String(key); // Convert key and IV from base64
                aes.IV = Convert.FromBase64String(iv);
    
                ICryptoTransform encryptor = aes.CreateEncryptor(aes.Key, aes.IV);
    
                using (MemoryStream ms = new MemoryStream())
                {
                    using (CryptoStream cs = new CryptoStream(ms, encryptor, CryptoStreamMode.Write))
                    {
                        using (StreamWriter sw = new StreamWriter(cs))
                        {
                            sw.Write(plainText);
                        }
                    }
                    return Convert.ToBase64String(ms.ToArray());
                }
            }
        }
    
        public static string Decrypt(string cipherText)
        {
            using (Aes aes = Aes.Create())
            {
                aes.Key = Convert.FromBase64String(key); // Convert key and IV from base64
                aes.IV = Convert.FromBase64String(iv);
    
                ICryptoTransform decryptor = aes.CreateDecryptor(aes.Key, aes.IV);
    
                using (MemoryStream ms = new MemoryStream(Convert.FromBase64String(cipherText)))
                {
                    using (CryptoStream cs = new CryptoStream(ms, decryptor, CryptoStreamMode.Read))
                    {
                        using (StreamReader sr = new StreamReader(cs))
                        {
                            return sr.ReadToEnd();
                        }
                    }
                }
            }
        }
    }

    Quick Explainer: EncryptionUtility for Secure Data Handling in Unity The EncryptionUtility is a static helper class that uses AES (Advanced Encryption Standard) to encrypt and decrypt data in Unity. It ensures that sensitive game data, such as save files, cannot be easily read or tampered with by unauthorized users.

    1. Overview of AES Encryption AES is a symmetric encryption algorithm, meaning the same key is used to encrypt and decrypt data. It’s highly secure and widely used in applications requiring strong data protection.

    This class uses: Key: A secret value that determines how the data is encrypted and decrypted. IV (Initialization Vector): A random value that ensures unique encryption for the same input.

    1. Key and IV Setup
      private static readonly string key = ""; // Replace with your generated key
      private static readonly string iv = "";  // Replace with your generated IV

      Key: A Base64-encoded string representing a 256-bit (32-byte) key. IV: A Base64-encoded string representing a 128-bit (16-byte) initialization vector.

    Important: Always generate a unique and secure key and IV for your application. You can generate these using a tool like OpenSSL or online Base64 key generators.

    1. Encrypt Method This method encrypts a plaintext string and returns it as a Base64-encoded string.

    2. Decrypt Method This method decrypts a Base64-encoded encrypted string and returns the original plaintext.

    3. Why Use This? Security: Encrypting save files protects player data from being read or modified.

    Tamper Resistance: Any unauthorized changes to the save file result in decryption failure.

    Ease of Integration: Works seamlessly with Unity’s save systems, such as JSON serialization.

    This isn’t the best Encryption for the keys and stored in the game but I added it for a good way to learn about it

    well I think that’s about all you need to Know for added save files there are a lot of pages talking about it if you give it a search

    has upvoted this post.
    #16647
    Terence
    Level 30
    Keymaster
    Helpful?
    Up
    0
    ::

    Awesome post Cam. Your game is looking great as well!

    #16650
    Cam
    Level 22
    Silver Supporter (Patron)
    Helpful?
    Up
    1
    ::

    @Terrace I also got the revive working, it stops players movement knockback all enemy’s on revival

    has upvoted this post.
    #16657
    SingleBigNameLOL
    Level 4
    Participant
    Helpful?
    Up
    1
    ::

    Amazing Cam, thank you so much for sharing the Save/Load feature! By the way, your game looks beautiful. :)

    has upvoted this post.
    #16664
    Terence
    Level 30
    Keymaster
    Helpful?
    Up
    0
    ::

    @Cam I will give it a look soon :)

    @SingleBigNameLOL you can ask Cam for a copy of the game if you want to try it as well!

    #16665
    SingleBigNameLOL
    Level 4
    Participant
    Helpful?
    Up
    0
    ::

    @Terence That would be amazing, but how could he share the game? Maybe through a link so I can download it?

    @Cam As Terence mentioned, I would be very grateful for a copy of the game. As you said, a lot has been changed compared to Terence’s tutorial game. I’m not sure if sharing the game for experimentation would be possible…

    #16666
    Terence
    Level 30
    Keymaster
    Helpful?
    Up
    0
    ::

    @SingleBigNameLOL he has published it on Steam. You can search Arcane Pursuit and see if you can find it.

    #16667
    SingleBigNameLOL
    Level 4
    Participant
    Helpful?
    Up
    0
    ::

    Thank you Terence, I’ll look for it. ^.^

    #16668
    Cam
    Level 22
    Silver Supporter (Patron)
    Helpful?
    Up
    1
    ::

    I’ll be able to send you a beta key so you can have a test copy, you can’t find it on steam until I make a trailer for it

    has upvoted this post.
    #16670
    SingleBigNameLOL
    Level 4
    Participant
    Helpful?
    Up
    1
    ::

    Ah yes, I actually searched the store and couldn’t find it. My Steam name is Fröstheimer, and I have a profile picture of a red cross. If you’d like to add me there to send the key, feel free. Thank you, Cam.

    has upvoted this post.
Viewing 10 posts - 1 through 10 (of 10 total)
  • You must be logged in to reply to this topic.

Go to Login Page →


Advertisement below: