Forum begins after the advertisement:
[General] Save/Load System
Home › Forums › Video Game Tutorial Series › Creating a Rogue-like Shoot-em Up in Unity › [General] Save/Load System
- This topic has 9 replies, 3 voices, and was last updated 2 weeks, 3 days ago by SingleBigNameLOL.
-
AuthorPosts
-
December 2, 2024 at 6:51 am #16645::
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
-
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.
-
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
-
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 }
- 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.
- 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.
- 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.
-
Encrypt Method This method encrypts a plaintext string and returns it as a Base64-encoded string.
-
Decrypt Method This method decrypts a Base64-encoded encrypted string and returns the original plaintext.
-
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. December 2, 2024 at 12:03 pm #16647December 2, 2024 at 8:40 pm #16650December 3, 2024 at 1:36 pm #16657December 3, 2024 at 9:31 pm #16664::@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!
December 4, 2024 at 7:54 am #16665::@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…
December 4, 2024 at 11:31 am #16666::@SingleBigNameLOL he has published it on Steam. You can search Arcane Pursuit and see if you can find it.
December 4, 2024 at 11:42 am #16667December 4, 2024 at 1:22 pm #16668December 4, 2024 at 1:45 pm #16670 -
-
AuthorPosts
- You must be logged in to reply to this topic.
Advertisement below: