-
-
Save mariotab28/f871f644483005502b2632d33a4941ea to your computer and use it in GitHub Desktop.
Code for a Save System I wrote for a clone of a popular mobile game (Mazes & More), made with Unity. The game data is serialized and turned into a JSON format string that is stored in Unity's PlayerPrefs. I used a hash code for verifying that the save data was not modified.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System.Collections; | |
using System.Security.Cryptography; | |
using System.Collections.Generic; | |
using System.Text; | |
using UnityEngine; | |
namespace MazesAndMore | |
{ | |
#region Auxiliar classes | |
/// <summary> | |
/// Completion state of a level | |
/// </summary> | |
public enum LevelState | |
{ | |
LOCKED, UNLOCKED, COMPLETED | |
} | |
/// <summary> | |
/// Data to be stored when saving a game | |
/// </summary> | |
public class GameData | |
{ | |
// Number of hints available for the player | |
public int numOfHints; | |
// List containing the completion state of each level | |
public List<LevelState[]> levelProgress; | |
} | |
#endregion | |
/// <summary> | |
/// Static class with the methods for saving/loading game data | |
/// </summary> | |
public static class SaveSystem | |
{ | |
#region Serializable Classes | |
/// <summary> | |
/// Class for storing level data as JSON-compatible types | |
/// </summary> | |
[System.Serializable] | |
public class JSONLevelGroup | |
{ | |
// List containing the completion state of each level | |
// (0 = LOCKED, 1 = UNLOCKED, 2 = COMPLETED) | |
public List<int> levelStates; | |
} | |
/// <summary> | |
/// Class for storing JSON-compatible save data | |
/// </summary> | |
[System.Serializable] | |
public class JSONSaveData | |
{ | |
// Hash code used to detect file modifications | |
public string saveId; | |
// Number of unlocked hints | |
public int hints; | |
// List containing level completion data for each level group (classic mode, ice mode, ...) | |
public List<JSONLevelGroup> levelGroups; | |
} | |
#endregion | |
#region Save and Load Methods | |
/// <summary> | |
/// Serializes player progress and stores it in PlayerPrefs | |
/// </summary> | |
/// <param name="data">Progress data to be saved</param> | |
public static void SaveGameData(GameData data) | |
{ | |
// Create a JSONSaveData with the game data | |
JSONSaveData jsonData = ToJSONSaveData(data); | |
// Generate the hash based on jsonData content | |
string hashCode = GenerateHashCode(jsonData); | |
// Add the hash to jsonData | |
jsonData.saveId = hashCode; | |
// Generate a JSON format string from jsonData and place it in PlayerPrefs | |
string json = JsonUtility.ToJson(jsonData); | |
PlayerPrefs.SetString("progress", json); | |
} // SaveGameData | |
/// <summary> | |
/// Loads the player progress (if no progress is found, saves the game) | |
/// </summary> | |
/// <param name="data">GameData variable to store the progress</param> | |
public static void LoadGameData(ref GameData data) | |
{ | |
string json = PlayerPrefs.GetString("progress"); | |
// Save the current state of the game if no progress was found | |
if (json == "") | |
{ | |
SaveGameData(data); | |
return; | |
} | |
// Build a JSONSaveData from the JSON string | |
JSONSaveData jsonData = JsonUtility.FromJson<JSONSaveData>(json); | |
// Verification of the hash code | |
if (!VerifyHashCode(jsonData)) return; | |
// Load the number of hints | |
data.numOfHints = jsonData.hints; | |
// Load the completion state of the levels | |
List<JSONLevelGroup> jsonGroups = jsonData.levelGroups; | |
for (int i = 0; i < data.levelProgress.Count; i++) | |
{ | |
JSONLevelGroup group = jsonGroups[i]; | |
for (int j = 0; j < group.levelStates.Count; j++) | |
data.levelProgress[i][j] = (LevelState)group.levelStates[j]; | |
} | |
} // LoadGameData | |
#endregion | |
/// <summary> | |
/// Turns given data into a JSON-combatible structure of data | |
/// </summary> | |
/// <param name="data">Game progress data</param> | |
/// <returns>JSONSaveData structure with the current progress data</returns> | |
static JSONSaveData ToJSONSaveData(GameData data) | |
{ | |
JSONSaveData jsonData = new JSONSaveData(); | |
// Unlocked hints number | |
jsonData.hints = data.numOfHints; | |
// Level completion data for each level group (classic mode, ice mode, ...) | |
List<JSONLevelGroup> groups = new List<JSONLevelGroup>(); | |
for (int g = 0; g < data.levelProgress.Count; g++) | |
{ | |
// List for storing the state of each level in the current group | |
List<int> states = new List<int>(); | |
int levelsLength = data.levelProgress[g].Length; | |
int level = 0; | |
bool locked = false; | |
// Stores the state of each level until finding the first LOCKED level (following levels will also be locked) | |
while (level < levelsLength && !locked) | |
{ | |
LevelState state = data.levelProgress[g][level]; | |
// Only store unlocked levels | |
if (state != LevelState.LOCKED) states.Add((int)state); | |
else locked = true; | |
level++; | |
} | |
// Generate level group data | |
JSONLevelGroup levels = new JSONLevelGroup(); | |
levels.levelStates = states; | |
// Add the group data to the list | |
groups.Add(levels); | |
} | |
// Set the jsonData level group list | |
jsonData.levelGroups = groups; | |
return jsonData; | |
} // ToJSONSaveData | |
/// <summary> | |
/// Generates a hexadecimal hash SHA-1 code based on the game data JSON structure | |
/// </summary> | |
/// <param name="data">JSON-combatible structure of data</param> | |
/// <returns>Hexadecimal hash SHA-1 code based on the game data</returns> | |
public static string GenerateHashCode(JSONSaveData data) | |
{ | |
// String to be used as input for the hash code generation | |
string input = ""; | |
if (data == null || data.levelGroups == null) | |
return input; | |
input += data.hints.ToString(); // Number of hints | |
foreach (JSONLevelGroup group in data.levelGroups) | |
for (int i = 0; i < group.levelStates.Count; i++) | |
input += group.levelStates[i].ToString(); // Level progress | |
// Generate the hash using SHA-1 algorithm | |
using (SHA1Managed sha1 = new SHA1Managed()) | |
{ | |
var hash = sha1.ComputeHash(Encoding.UTF8.GetBytes(input)); | |
var sb = new StringBuilder(hash.Length * 2); | |
// Write the hash in hexadecimal format and store it in sb | |
foreach (byte b in hash) | |
sb.Append(b.ToString("x2")); | |
return sb.ToString(); | |
} | |
} | |
/// <summary> | |
/// Verifies that the hash code contained in jsonData corresponds to its stored content. | |
/// If the hash is incorrect, the saved progress is overwritten. | |
/// </summary> | |
/// <param name="jsonData">Data containing the hash to be verified</param> | |
/// <returns>true if the hash code is correct, false otherwise</returns> | |
public bool VerifyHashCode(JSONSaveData jsonData) | |
{ | |
string saveDataHash = jsonData.saveId; | |
string testHash = GenerateHashCode(jsonData); // Verification hash | |
if (saveDataHash != testHash) // Check if the verification and stored hashes match | |
{ | |
SaveGameData(data); // Overwrite the saved progress if hashes don't match | |
return false; | |
} | |
return true; | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment