{"id":507,"date":"2021-08-28T17:07:37","date_gmt":"2021-08-28T17:07:37","guid":{"rendered":"https:\/\/bronsonzgeb.com\/?p=507"},"modified":"2021-08-28T17:07:38","modified_gmt":"2021-08-28T17:07:38","slug":"save-data-with-binarywriter-and-binaryreader","status":"publish","type":"post","link":"https:\/\/bronsonzgeb.com\/index.php\/2021\/08\/28\/save-data-with-binarywriter-and-binaryreader\/","title":{"rendered":"Save Data with BinaryWriter and BinaryReader"},"content":{"rendered":"\n<p>This article will explore saving and loading data to a binary format through <code>BinaryWriter<\/code> and <code>BinaryReader<\/code>. Additionally, I&#8217;ll provide tips for structuring your save data. Let&#8217;s get into it!<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">What&#8217;s the plan?<\/h2>\n\n\n\n<p>We&#8217;ll build a save data manager that&#8217;ll take a <code>SaveData<\/code> object and either write it to a file or read from an existing file. We&#8217;ll support Binary through <code>BinaryWriter<\/code> and <code>BinaryReader<\/code> and JSON through Unity&#8217;s built-in <code>JsonUtility<\/code>. The fundamental architecture doesn&#8217;t change from one format to the next, so you could always extend the manager to support other backends like MessagePack or ProtoBuf.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Save Data<\/h2>\n\n\n\n<p>As I mentioned, the save data manager will read and write a <code>SaveData<\/code> object. The <code>SaveData<\/code> object is a class that contains everything we need to recreate the current game state, such as the player&#8217;s health and position and the active scene. A more complicated example could be the status and id of every single entity in the world. In one turn-based prototype I wrote, I store every entity and every tile in the world for every turn that has passed so that players can rewind to any previous turn even after quitting and loading the game.<\/p>\n\n\n\n<p>I did my best to recreate a simple example that demonstrates as many principles as possible. Ideally, it&#8217;s easy enough to understand, and all you have to do is extend it to support the data your project requires. You can find a link to the Github repository for the example project at the end of this post.<\/p>\n\n\n\n<p>So in this example, we have a game with a bunch of entities. Each entity has an id that links to a prefab through a <code>ScriptableObject<\/code>. When we save, we grab all the entities in the world and store their id with their position. Here&#8217;s how the <code>SaveData<\/code> looks.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>using UnityEngine;\n\npublic class SaveData : ScriptableObject\n{\n    public string&#91;] EntityUuids;\n    public Vector3&#91;] EntityPositions;\n\n    public void SetEntities(Entity&#91;] entities)\n    {\n        EntityUuids = new string&#91;entities.Length];\n        EntityPositions = new Vector3&#91;entities.Length];\n        for (int i = 0; i &lt; entities.Length; ++i)\n        {\n            EntityUuids&#91;i] = entities&#91;i].EntityDescriptor.Uuid;\n            EntityPositions&#91;i] = entities&#91;i].transform.position;\n        }\n    }\n}<\/code><\/pre>\n\n\n\n<p>As mentioned, it holds an array of ids and one of the positions. Also, it contains a helper function that takes an array of entities and isolates the data we need to save. Let&#8217;s explore how to save this with BinaryWriter.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">BinaryReader and BinaryWriter<\/h2>\n\n\n\n<p><code>BinaryReader<\/code> and <code>BinaryWriter<\/code> are tools built into dotNET that read and write binary data. It&#8217;s faster and more efficient on storage than Unity&#8217;s built-in JsonUtility, which stores data in the JSON format. The limitation of <code>BinaryReader<\/code> is that you must read the information in the same order you wrote it. What I mean is, JSON allows you to search for fields based on their name, but as far as I know, <code>BinaryReader<\/code> assumes you know the order in which you wrote the elements. By the way, if you&#8217;re familiar with <code>BinaryFormatter<\/code> but haven&#8217;t heard the news,&nbsp;<a rel=\"noreferrer noopener\" target=\"_blank\" href=\"https:\/\/docs.microsoft.com\/en-us\/dotnet\/standard\/serialization\/binaryformatter-security-guide\">you should never use it<\/a>. It is &#8220;insecure and can&#8217;t be made secure.&#8221; Another limitation of binary data is it isn&#8217;t human-readable. The JSON format opens like a text file and is relatively readable. This readability is helpful when debugging save files. On the other hand, you need to use a hex editor to open binary files, and even then, they mostly look like gibberish. Anyway, Let&#8217;s see how we might write our <code>SaveData<\/code> using <code>BinaryWriter<\/code>.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>public static void SaveBinary(string fileName, SaveData saveData)\n{\n    var fileFullPath = $\"{_saveFolderPath}\/{fileName}\";\n    using (BinaryWriter writer = new BinaryWriter(File.Open(fileFullPath, FileMode.OpenOrCreate)))\n    {\n        writer.Write(saveData.EntityUuids.Length);\n        for (int i = 0; i &lt; saveData.EntityUuids.Length; ++i)\n        {\n            writer.Write(saveData.EntityUuids&#91;i]);\n            var position = saveData.EntityPositions&#91;i];\n            writer.Write(position.x);\n            writer.Write(position.y);\n            writer.Write(position.z);\n        }\n    }\n\n    Debug.Log($\"Saved Binary to {fileFullPath}\");\n}<\/code><\/pre>\n\n\n\n<p>In this example, we open (or create) a new file with the desired name. Then, we write the length of the <code>EntityUuids<\/code> array, which is how many entities exist in the world. Knowing the number of entities is necessary because there&#8217;s no other way to know when the array of entities ends while we read back the information. Afterwards, we write the id of the entity and its X, Y and Z coordinates.<\/p>\n\n\n\n<p>Now let&#8217;s see how to read the information.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>public static void LoadBinary(string fileName, SaveData saveData)\n{\n    var fileFullPath = $\"{_saveFolderPath}\/{fileName}\";\n    using (BinaryReader reader = new BinaryReader(File.Open(fileFullPath, FileMode.Open)))\n    {\n        var entityCount = reader.ReadInt32();\n        saveData.EntityUuids = new string&#91;entityCount];\n        saveData.EntityPositions = new Vector3&#91;entityCount];\n        for (int i = 0; i &lt; entityCount; ++i)\n        {\n            saveData.EntityUuids&#91;i] = reader.ReadString();\n            saveData.EntityPositions&#91;i] = new Vector3(reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle());\n        }\n    }\n\n    Debug.Log($\"Loaded Binary from {fileFullPath}\");\n}<\/code><\/pre>\n\n\n\n<p>It&#8217;s very similar to the saving function. First, we read the number of entities and create new arrays that are big enough to fit them. Then, one by one, we read the id and X, Y and Z coordinates while storing them in the <code>SaveData<\/code>. By the way, a critical distinction between reading and writing is that when reading, you have to specify the <code>type<\/code> of the next value. That&#8217;s why you have to know the data format beforehand and why we have to know precisely how many entities are in the <code>SaveData<\/code> file. However, given this limitation, what do we do if we need to change the save format? Assuming that you stick with BinaryReader, I recommend versioning your save file.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Save Data Version<\/h2>\n\n\n\n<p>I store the version at the beginning of my save files. By that, I mean simply writing a number before any other data. Once you do this, you have to hang on to the method used to load data for every version. That way, you can read older save files and convert them to the most up-to-date format. Ideally, you&#8217;d do this from the start; otherwise, it&#8217;s tough to detect the version later, and you risk losing user data. So update <code>SaveData<\/code> to hold the version.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>public class SaveData : ScriptableObject\n{\n    <strong>public int Version;<\/strong>\n    public string&#91;] EntityUuids;\n    public Vector3&#91;] EntityPositions;\n}<\/code><\/pre>\n\n\n\n<p>Then we can modify the <code>SaveManager<\/code> to read and write the version.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>public class SaveManager : MonoBehaviour\n{\n    static string _saveFolderPath;\n\n    <strong>public const int CurrentSaveVersion = 1;<\/strong>\n\n    void OnEnable()\n    {\n        _saveFolderPath = $\"{Application.persistentDataPath}\/Saves\";\n\n        if (!Directory.Exists(_saveFolderPath))\n            Directory.CreateDirectory(_saveFolderPath);\n    }\n\n    public static void SaveBinary(string fileName, SaveData saveData)\n    {\n        var fileFullPath = $\"{_saveFolderPath}\/{fileName}\";\n        using (BinaryWriter writer = new BinaryWriter(File.Open(fileFullPath, FileMode.OpenOrCreate)))\n        {\n            <strong>writer.Write(saveData.Version);<\/strong>\n            writer.Write(saveData.EntityUuids.Length);\n            for (int i = 0; i &lt; saveData.EntityUuids.Length; ++i)\n            {\n                writer.Write(saveData.EntityUuids&#91;i]);\n                var position = saveData.EntityPositions&#91;i];\n                writer.Write(position.x);\n                writer.Write(position.y);\n                writer.Write(position.z);\n            }\n        }\n\n        Debug.Log($\"Saved Binary to {fileFullPath}\");\n    }\n\n    public static void LoadBinary(string fileName, SaveData saveData)\n    {\n        var fileFullPath = $\"{_saveFolderPath}\/{fileName}\";\n        using (BinaryReader reader = new BinaryReader(File.Open(fileFullPath, FileMode.Open)))\n        {\n            <strong>saveData.Version = reader.ReadInt32();<\/strong>\n            var entityCount = reader.ReadInt32();\n            saveData.EntityUuids = new string&#91;entityCount];\n            saveData.EntityPositions = new Vector3&#91;entityCount];\n            for (int i = 0; i &lt; entityCount; ++i)\n            {\n                saveData.EntityUuids&#91;i] = reader.ReadString();\n                saveData.EntityPositions&#91;i] = new Vector3(reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle());\n            }\n        }\n\n        Debug.Log($\"Loaded Binary from {fileFullPath}\");\n    }\n}<\/code><\/pre>\n\n\n\n<p>You can read the version number from the save file and branch to the corresponding load method as you add new versions.<\/p>\n\n\n\n<p>Using <code>BinaryReader<\/code> allows precise control over save file loads, which is both a blessing and a curse. Unity&#8217;s built-in <code>JsonUtility<\/code> tries to associate fields in the JSON file to their counterpart in the C# object. If it fails, that data goes unassigned. This is the equivalent of renaming a serialized field in a <code>MonoBehaviour<\/code> and losing all the assigned data. However, here&#8217;s what the save and load methods look like using <code>JsonUtility<\/code> for comparison.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>public static void SaveJson(string fileName, SaveData saveData)\n{\n    var fileFullPath = $\"{_saveFolderPath}\/{fileName}\";\n    var jsonString = JsonUtility.ToJson(saveData);\n    File.WriteAllText(fileFullPath, jsonString);\n\n    Debug.Log($\"Saved JSON to {fileFullPath}\");\n}\n\npublic static void LoadJson(string fileName, SaveData saveData)\n{\n    var fileFullPath = $\"{_saveFolderPath}\/{fileName}\";\n    var jsonString = File.ReadAllText(fileFullPath);\n    JsonUtility.FromJsonOverwrite(jsonString, saveData);\n\n    Debug.Log($\"Loaded JSON from {fileFullPath}\");\n}<\/code><\/pre>\n\n\n\n<p>They&#8217;re much less verbose but don&#8217;t allow you to handle schema changes cleanly. In other words, if you rename fields in <code>SaveData<\/code>, you&#8217;ll lose data if you load an older save file.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Wrap-up<\/h2>\n\n\n\n<p>So in this post, we saw how to use <code>BinaryWriter<\/code> and <code>BinaryReader<\/code> to save and load data. However, we didn&#8217;t see what to do with that data afterwards. That topic is interesting because it explores how to architect your data to be serialized. This includes mapping an id to a prefab to be saved and loaded. That may become a future post, but until then, you can see a simple example in the Github project linked at the end.<\/p>\n\n\n\n<p><strong>Explore the Github project is available&nbsp;<\/strong><a rel=\"noreferrer noopener\" target=\"_blank\" href=\"https:\/\/github.com\/bzgeb\/BinarySaveDataExample\"><strong>here<\/strong><\/a><strong>. If you like my work,&nbsp;<\/strong><a rel=\"noreferrer noopener\" target=\"_blank\" href=\"https:\/\/bronsonzgeb.com\/index.php\/join-my-mailing-list\/\"><strong>join my mailing list<\/strong><\/a><strong>&nbsp;to be notified when I write a new post.<\/strong><\/p>\n","protected":false},"excerpt":{"rendered":"<p>This article will explore saving and loading data to a binary format through BinaryWriter and BinaryReader. Additionally, I&#8217;ll provide tips for structuring your save data. Let&#8217;s get into it! What&#8217;s the plan? We&#8217;ll build a save data manager that&#8217;ll take a SaveData object and either write it to a file or read from an existing [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":506,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[1,53],"tags":[51,50,8,49,52,5],"class_list":["post-507","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-unity-programming","category-utilities","tag-binaryreader","tag-binarywriter","tag-game-development","tag-persistent-data","tag-save-game","tag-unity"],"_links":{"self":[{"href":"https:\/\/bronsonzgeb.com\/index.php\/wp-json\/wp\/v2\/posts\/507","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/bronsonzgeb.com\/index.php\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/bronsonzgeb.com\/index.php\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/bronsonzgeb.com\/index.php\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/bronsonzgeb.com\/index.php\/wp-json\/wp\/v2\/comments?post=507"}],"version-history":[{"count":5,"href":"https:\/\/bronsonzgeb.com\/index.php\/wp-json\/wp\/v2\/posts\/507\/revisions"}],"predecessor-version":[{"id":512,"href":"https:\/\/bronsonzgeb.com\/index.php\/wp-json\/wp\/v2\/posts\/507\/revisions\/512"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/bronsonzgeb.com\/index.php\/wp-json\/wp\/v2\/media\/506"}],"wp:attachment":[{"href":"https:\/\/bronsonzgeb.com\/index.php\/wp-json\/wp\/v2\/media?parent=507"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/bronsonzgeb.com\/index.php\/wp-json\/wp\/v2\/categories?post=507"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/bronsonzgeb.com\/index.php\/wp-json\/wp\/v2\/tags?post=507"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}