Get 10% off orders above 50% with SEPT2025.
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 14 replies, 5 voices, and was last updated 6 days, 7 hours ago by
Alp Apustaja.
-
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
and 1 other person have upvoted this post. December 2, 2024 at 12:03 pm #16647::Awesome post Cam. Your game is looking great as well!
- 1 anonymous person
December 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 #16670September 3, 2025 at 5:25 pm #18795::Hey guys! I want to use this awesome save system (Thank you Cam), but I always get an error regarding the key lenght: CryptographicException: Specified key is not a valid size for this algorithm. How many characters should those keys have in order to work? Thank you all for your great work! :)
September 3, 2025 at 5:35 pm #18797::It sounds like you’re encountering an issue with the length of the cryptographic key you’re using. The required key length can vary depending on the specific algorithm you’re using (AES, DES, etc.).
For AES (Advanced Encryption Standard), the valid key lengths are:
- 128 bits (16 bytes, 16 characters if using ASCII)
- 192 bits (24 bytes, 24 characters if using ASCII)
- 256 bits (32 bytes, 32 characters if using ASCII)
Make sure that the key you are providing matches one of these lengths. If you’re using a string as the key, ensure that it is the correct number of bytes. You might need to convert it or pad it accordingly.
If you’re using a different algorithm, please specify which one, and I can provide the correct key lengths for that algorithm.
Also, if you’re generating keys programmatically, consider using a secure random generator that produces keys of the appropriate size automatically.
September 3, 2025 at 7:23 pm #18798::Can anyone help me what to use on this page to generate working keys? https://generate-random.org/encryption-key-generator
September 3, 2025 at 7:25 pm #18799::To generate a working key for AES encryption using the website you mentioned, follow these steps:
-
Choose the Key Length: Depending on your requirements, select one of the following key lengths:
- For AES-128, select a 16-character key.
- For AES-192, select a 24-character key.
- For AES-256, select a 32-character key.
-
Generate the Key:
- On the page, look for the option that allows you to specify the length of the key. Enter the desired length (16, 24, or 32 characters).
- Click the “Generate” button to create a random key.
-
Copy the Key: Once the key is generated, make sure to copy it for use in your application.
-
Character Type Consideration: If the system you’re using requires specific character types (like only alphanumeric characters), make sure to check if the generated key meets those requirements. You may want to manually adjust the key if it contains special characters that are not allowed.
-
Test the Key: After generating the key, test it in your application to ensure it works without throwing the
CryptographicException
.
By following these steps, you should be able to generate a valid key that works with your encryption system. If you encounter further issues, feel free to ask!
September 3, 2025 at 7:55 pm #18800September 3, 2025 at 9:41 pm #18801September 10, 2025 at 4:32 pm #18834::So I’m able to create the first key but I’m not sure about the second one. Do I have to use the IV 128-bit (16-byte) vector from the same seed as the 256-bit (32-byte) key? Thank you for your help. :)
September 10, 2025 at 4:35 pm #18835::When working with AES encryption, the initialization vector (IV) and the encryption key serve different purposes, and they don’t have to come from the same seed or be derived in the same way. Here are some key points to consider:
-
Key and IV Length:
- The AES key can be 128 bits (16 bytes), 192 bits (24 bytes), or 256 bits (32 bytes).
- The IV must always be 128 bits (16 bytes) for AES, regardless of the key size.
-
IV Generation:
- The IV should be unique for each encryption operation to ensure security, but it does not need to be secret. You can generate a random IV for each encryption operation.
- You can create the IV using a secure random number generator.
-
Using the Same Seed:
- While you can generate both the key and IV from the same seed if you want, it’s not necessary or recommended. In fact, using a random IV each time is a common practice to enhance security.
- If you do use the same seed, ensure that your key and IV generation algorithm produces a valid key and a valid IV.
-
Storing the IV:
- When you encrypt data, you typically need to store or transmit the IV along with the ciphertext, as it is required for decryption.
In summary, you can generate the IV independently of the key, but ensure that it’s a valid size (16 bytes) and ideally generated randomly for each encryption session. If you have more questions or need assistance with your code, feel free to ask!
-
-
AuthorPosts
- You must be logged in to reply to this topic.