So, I’m working on a project where saving and loading the Random.state seems like a necessity. I’ve made it work and the first time I press play mode, everything works, including saving and loading the different states to and from a file, and always getting the same outcome after loading, no issues there.
However, next time I run play mode, the editor freezes and on task manager I can see that the Unity Editor process is not responding.
After some debugging, I discovered it’s coming from this code:
do
{
x = Random.Range(0, 3);
y = Random.Range(0, 10);
//Other stuff here
} while (/* Statements that use X and Y */);
For some reason, if I save and load Random.state in one play mode, the next time I run the project, Random.Range always returns 0, which (without getting into more of my code) makes the while condition never be false to stop the while loop, and freezes the editor because of the infinite loop.
There is no saving/loading right off the bat when I press play, so there shouldn’t be interference caused by serialization between plays, and the save file is also not the culprit, because after force quitting the editor and opening the project again I can run play mode and it just works as supposed to.
This leads me to believe that this is some internal Unity shenanigans I’m not educated in enough to understand, so any help or direction would be great.
[System.Serializable]
public class GameData
{
public string sceneName;
public Random.State state;
public SerializableDictionary<string, PCDataStruct> playerCharacterDict;
public SerializableDictionary<string, NPCDataStruct> npcCharacterDict;
// the values defined in this constructor will be the default values
// the game starts with when there's no data to load
public GameData()
{
sceneName = "Level1";
playerCharacterDict = new SerializableDictionary<string, PCDataStruct>();
npcCharacterDict = new SerializableDictionary<string, NPCDataStruct>();
}
}
This problem is confusing, because serialization only affects Random.state after I press the save or load button on my scene, not just by entering play mode, so if the issue happened during that time it would make sense. The worst part is that saving/loading works correctly during the first time I play the scene, the state is saved and the next Random result is the same after loading, but relaunching the scene after stopping play mode breaks Random.
To reiterate, my scene does not save or load at all when I press play, only when I press buttons in the UI to do so, but it makes it really weird that this problem seems to have nothing to do with serializing Random.state, but also only showed up after I implemented serialization for Random.state.
Update: Turns out I’m dumb and Random.state wasn’t actually being correctly serialized, it just saved as an empty list in the JSON. Every time I loaded through the file, Random.state was just initialized with empty values, and for some reason I don’t know, Random.state persists across different runtimes. Also Random.Range always returns the lowest possible number in the range everytime.
That leaves the question: so how do I serialize Random.state correctly? I’m already using Newtonsoft Json .Net which fixed everything else, so I’m somewhat stumped…
Edit: FileDataHandler class:
public class FileDataHandler
{
private string dataDirPath = "";
private string dataFileName = "";
private bool useEncryption = false;
private readonly string encryptionCodeWord = "word";
public FileDataHandler(string dataDirPath, string dataFileName, bool useEncryption)
{
this.dataDirPath = dataDirPath;
this.dataFileName = dataFileName;
this.useEncryption = useEncryption;
}
public GameData Load()
{
// use Path.Combine to account for different OS's having different path separators
string fullPath = Path.Combine(dataDirPath, dataFileName);
GameData loadedData = null;
if (File.Exists(fullPath))
{
try
{
// load the serialized data from the file
string dataToLoad = "";
using (FileStream stream = new(fullPath, FileMode.Open))
{
using StreamReader reader = new(stream);
{
dataToLoad = reader.ReadToEnd();
}
}
// optionally decrypt the data
if (useEncryption)
{
dataToLoad = EncryptDecrypt(dataToLoad);
}
// deserialize the data from Json back into the C# object
loadedData = JsonConvert.DeserializeObject<GameData>(dataToLoad);
}
catch (Exception e)
{
Debug.LogError("Error occured when trying to load data from file: " + fullPath + "\n" + e);
}
}
return loadedData;
}
public void Save(GameData data)
{
// use Path.Combine to account for different OS's having different path separators
string fullPath = Path.Combine(dataDirPath, dataFileName);
try
{
// create the directory the file will be written to if it doesn't already exist
Directory.CreateDirectory(Path.GetDirectoryName(fullPath));
// serialize the C# game data object into Json
string dataToStore = JsonConvert.SerializeObject(data, Formatting.Indented, new JsonSerializerSettings
{
ReferenceLoopHandling = ReferenceLoopHandling.Ignore,
});
// optionally encrypt the data
if (useEncryption)
{
dataToStore = EncryptDecrypt(dataToStore);
}
// write the serialized data to the file
using FileStream stream = new(fullPath, FileMode.Create);
{
using StreamWriter writer = new(stream);
{
writer.Write(dataToStore);
}
}
}
catch (Exception e)
{
Debug.LogError("Error occured when trying to save data to file: " + fullPath + "\n" + e);
}
}
// the below is a simple implementation of XOR encryption
private string EncryptDecrypt(string data)
{
string modifiedData = "";
for (int i = 0; i < data.Length; i++)
{
modifiedData += (char) (data[i] ^ encryptionCodeWord[i % encryptionCodeWord.Length]);
}
return modifiedData;
}
}
I just put a public Random.State variable into a MonoBehaviour, copied it from Random.state, and looked at the Inspector. It shows four fields, S0 S1 S2 S3 (a common setup for multi-PRNGs). All large integers, not zero. It therefore CAN be serialized by Unity’s JsonUtility.
So I tried string s = UnityEngine.JsonUtility.ToJson(w); and got a result like {"state":{"s0":-499403572,"s1":-1864552799,"s2":787603492,"s3":-1826522587}}. [The w is a wrapper class instance, since Random.State is a struct.]
You can just serialize the state with Unity’s JsonUtility into a string, then save that string in your Newtonsoft data structure. Reverse to make use of it.
Over here I posted a wrapper for the State struct so you can actually read out the 4 values individually. Note that the implementation of XorShift that Unity uses requires a “valid” state. XorShift requires that there are some non-zero values in the state. When all values are 0, nothing will ever happen as the computation are just shift and xor combinations between the 4 internal state variables. So when all are 0, if can never reach any other state than 0.