Skip to content

Instantly share code, notes, and snippets.

@mariotab28
Last active July 5, 2022 10:49
Show Gist options
  • Save mariotab28/f871f644483005502b2632d33a4941ea to your computer and use it in GitHub Desktop.
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.
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