This is the sixth tutorial in a series about Object Management. It covers saving more game state, in addition to the spawned shapes and level index.

This tutorial is made with Unity 2017.4.4f1.

Saving Randomness

The point of using randomness when spawning shapes is to get unpredictable results. But this isn't always desirable. Suppose you save the game, then spawn a few more shapes. Then you load, and spawn the same amount of shapes again. Should you end up with the exact same shapes, or different ones? Currently, you'll get different ones. But the other option is equally valid, so let's support it.

The numbers generated by Unity's Random methods aren't truly random. They're pseudorandom. It's a sequence of numbers generated by a mathematical formula. At the start of the game, this sequence gets initialized with an arbitrary seed value, based on the current time. If you start a new sequence with the same seed, you'll get the same numbers.

Writing Random State Storing the initial seed value is not enough, because that would bring us back to the start of the sequence, instead of the point in the sequence when the game got saved. But Random must keep track of where in the sequence it is. If we can get to this state, then we can later restore it, continuing the old sequence. The random state is defined as a State struct, nested inside the Random class. So we can declare a field or parameter of this type, Random.State . To save it, we have to add a method to GameDataWriter that can write such a value. Let's add this method now, but leave its implementation for later. public void Write (Random.State value) {} With this method, we can save the random state of the game. We'll do this at the start of Game.Save , right after writing the shape count. Again, increment the save version to signal a new format. const int saveVersion = 3 ; … public override void Save (GameDataWriter writer) { writer.Write(shapes.Count); writer.Write(Random.state); writer.Write(loadedLevelBuildIndex); … }

Reading Random State To read the random state, add a ReadRandomState method to GameDataReader . As we're not writing anything yet, skip reading anything for now. Instead, return the current random state, so nothing changes. The current state can be found via the static Random.state property. public Random.State ReadRandomState () { return Random.state; } Setting the random state is done via the same property, which we'll do in Game.Load , but only for save file versions 3 and higher. public override void Load (GameDataReader reader) { … int count = version <= 0 ? -version : reader.ReadInt(); if (version >= 3) { Random.state = reader.ReadRandomState(); } StartCoroutine(LoadLevel(version < 2 ? 1 : reader.ReadInt())); … }

JSON Serialization Random.State contains four floating-point numbers. However, they aren't publicly accessible, so it is not possible for us to simply write them. We have to use an indirect approach instead. Fortunately, Random.State is a serializable type, so it is possible to convert it to a string representation of the same data, using the ToJson method of Unity's JsonUtility class. This gives us a JSON string. To see what that looks like, log it to the console. public void Write (Random.State value) { Debug.Log(JsonUtility.ToJson(value)); } What does Json mean? The proper spelling is JSON, all capital letters. It stands for JavaScript Object Notation. It defines a simple human-readable data format. After saving a game, the console will now log a string of four numbers named s0 through s3, between curly brackets. Something like {"s0":-1409360059,"s1":1814992068,"s2":-772955632,"s3":1503742856}. We'll write this string to our file. If you were to open the saved file with a text editor, you'll be able to see this string near the beginning of the file. public void Write (Random.State value) { writer.Write (JsonUtility.ToJson(value)); } In ReadRandomState , read this string by invoking ReadString and then use JsonUtility.FromJson to convert it back to a proper random state. public Random.State ReadRandomState () { return JsonUtility.FromJson(reader.ReadString()) ; } Besides the data, FromJson also needs to know the type of whatever it's supposed to create from the JSON data. We can use the generic version of the method, specifying that it should create a Random.State value. public Random.State ReadRandomState () { return JsonUtility.FromJson <Random.State> (reader.ReadString()); }

Decoupling Levels Our game now saves and restores the random state. You can verify this by beginning a game, saving, creating a few shapes, then loading, and creating the exact same shapes again. But you can go a step further. You could even begin a new game after loading, and still create the same shapes after that. So we can influence the randomness of a new game by loading a game right before it. This is not desirable. Ideally, the randomness of distinct games is separate, as if we restarted the whole game. We can achieve this by seeding a new random sequence each time we begin a new game. To pick a new seed value, we have to use randomness. We can use Random.value for that, but have to make sure that these values come from their own random sequence. To do this, add a main random state field to Game . At the start of the game, set it to the random state that was initialized by Unity. Random.State mainRandomState; … void Start () { mainRandomState = Random.state; … } When the player begins a new game, the first step is now to restore the main random state. Then grab a random value and use that as the seed to initialize a new pseudorandom sequence, via the Random.InitState method. void BeginNewGame () { Random.state = mainRandomState; int seed = Random.Range(0, int.MaxValue); Random.InitState(seed); … } To make the seeds a little more unpredictable, we'll mix them with the current play time, accessible via Time.unscaledTime . The bitwise exclusive-OR operator ^ is good for this. int seed = Random.Range(0, int.MaxValue) ^ (int)Time.unscaledTime ; What does exclusive-OR do? For each bit, the result is 1 if exactly one of the two inputs is 1 and the other is 0. Otherwise, the result is 0. In other words, whether the inputs are different. Because it is a bit manipulation, the result isn't mathematically obvious, like addition would be. To keep track of the progression of the main random sequence, store its state after grabbing the next value, before initializing the state for the new game. Random.state = mainRandomState; int seed = Random.Range(0, int.MaxValue); mainRandomState = Random.state; Random.InitState(seed); Now loading games and what you do in each game no longer affects the randomness of other games played during the same session. But to make sure this works correctly we also have to invoke BeginNewGame for the first game of each session. void Start () { … BeginNewGame(); StartCoroutine(LoadLevel(1)); }