This is the 12th part of a tutorial series about hexagon maps. By now we can create reasonably interesting maps. It's time to save them.

Although we have removed the color data from our cells, the map should still work as before. The only difference is that the default color is now the first in the array, which is yellow in my case.

Adjust EditCell so the terrain type index is assigned to the edited cell, when appropriate.

Use this method as a replacement for the now missing SelectColor method. Connect the color widgets in the UI to SetTerrainTypeIndex , keeping everything else the same. This means that a negative index is still used to indicate that the color shouldn't be changed.

Inside HexMapEditor , remove all the code that deals with colors. That will fix the compile error as well.

The color property can use this index to retrieve the appropriate color. It can no longer be directly set, so remove that part. That will produce a compile error, which we'll fix shortly.

Remove the color field from HexCell . Instead, we'll store an index. And instead of a color index, we'll use a more generic terrain type index.

Configure the new colors so they match the public array of the hex map editor.

And as we'll no longer directly assign colors to cells, get rid of the default color.

Like with other global data like the noise, we can initialize these colors via HexGrid .

If cells no longer have color data, then it should be available somewhere else. The most convenient place is HexMetrics . So let's add a color array to it.

Likewise, the exact color of a cell need not be stored. It's ok to remember that a cell is green. But the exact shade of green might change if we adjust the visual style. To do this, we could store the color index, instead of the colors themselves. In fact, we can suffice with storing this index in cells at run time as well, instead of the actual colors. That allows us to upgrade to a more advanced terrain visualization later.

When saving a map, we do not need to store all the data that we keep track of at run time. For example, we only have to remember the elevation level of a cell. Its actual vertical position is derived from it, so need not be stored. In fact, it is better that we don't store those derived metrics. That way, the map data remains valid, even if later on we decide to adjust the elevation offset. The data is separate from the presentation.

Storing Data in a File

We'll use HexMapEditor to control saving and loading our map. Create two methods to take care of this, leaving them empty for now.

public void Save () { } public void Load () { }

Add two buttons to the UI, via GameObject / UI / Button. Hook them up to the Save and Load methods, and give them appropriate labels. I put them at the bottom of the right panel.

Save and Load buttons.

File Location To save a map, we have to store it somewhere. As most games do, we'll store the data in a file. But where on the player's file system should this file be put? The answer depends on which operating system the game runs on. Each has a different convention for storing application-specific files. We don't need to know these conventions. Unity knows the appropriate path, which we can retrieve via Application.persistentDataPath . You can check what it is for you, by logging it in Save and clicking the button while in play mode. public void Save () { Debug.Log(Application.persistentDataPath); } On desktops, the path will contain the company and product name. Both the editor and builds use this path. You can adjust the names via Edit / Project Settings / Player. Company and product name. Why can't I find the Library folder on a Mac? The Library folder is often hidden. How to make it visible depends on the OS X version. Unless you have an old version, select your home folder in Finder and go to Show View Options. There's a checkbox for the Library folder. What about WebGL? WebGL games cannot access the user's file system. Instead, all file operations will be directed to an in-memory file system instead. This is transparent to us. However, to persist the data, you'll have to manually instruct the webpage to flush the data to the browser's storage.

Creating a File To create a file, we have to use classes from the System.IO namespace. So add a using statement for it above the HexMapEditor class. using UnityEngine; using UnityEngine.EventSystems; using System.IO; public class HexMapEditor : MonoBehaviour { … } First, we need to create the full path of the file. We'll use test.map as the file's name. It should be appended to the persistent data path. Whether we have to put a slash or a backslash between them depends on the platform. The Path.Combine method will take care of this for us. public void Save () { string path = Path.Combine(Application.persistentDataPath, "test.map"); } Next, we have to access the file at this location. We do this with the File.Open method. Because we want to write data to this file, we have to use its create mode. That will either create a new file at the provided path, or replace the file if it already existed. string path = Path.Combine(Application.persistentDataPath, "test.map"); File.Open(path, FileMode.Create); The result of invoking this method is an open data stream, linked to this file. We can use it to write data into the file. And we must make sure to close the stream, once we no longer need it. string path = Path.Combine(Application.persistentDataPath, "test.map"); Stream fileStream = File.Open(path, FileMode.Create); fileStream.Close(); At this point, pressing the Save button will create a test.map file in the folder specified by the persistent data path. If you inspect this file, you'll find that it is empty, with a size of zero bytes. That's because we haven't written anything to it yet.

Writing to a File To actually put data into the file, we need a way to stream data to it. The most basic way to do this is by using a BinaryWriter . These objects allow us to write primitive data to any stream. Create a new BinaryWriter object, with our file stream as an argument. Closing the writer will also close the stream that it uses. So we don't need to keep a direct reference to the stream anymore. string path = Path.Combine(Application.persistentDataPath, "test.map"); BinaryWriter writer = new BinaryWriter( File.Open(path, FileMode.Create) ) ; writer .Close(); We can use the BinaryWriter.Write method to send data into the stream. There is a Write method variant for all primitive types, like integers and floats. It can also write strings. Let's try writing the integer 123. BinaryWriter writer = new BinaryWriter(File.Open(path, FileMode.Create)); writer.Write(123); writer.Close(); Press our Save button, and inspect test.map again. Its size is now four bytes. That's because the size of an integer is four bytes. Why does my file browser report that the file takes up more space? That's because file systems partitions space in blocks of bytes. It doesn't track single bytes. As test.map is currently only four bytes, it ends up claiming a single block of storage space. Note that we are storing binary data, not human-readable text. So if you were to open our file in a text editor, you would see gibberish. You might see the character {, followed by either nothing or some placeholder characters. You could open the file with a hex editor. In that case, you would see 7b 00 00 00. Those are the four bytes of our integer, displayed using hexadecimal notation. That's 123 0 0 0 when using regular decimal numbers. In binary notation, the first byte is 01111011. The ASCII code for { is 123, which is why a text editor might show that character. ASCII 0 is the null character, which doesn't correspond to a valid visible character. The other three bytes are zero, because we wrote a number that is smaller than 256. Had we written 256, then the hex editor would have shown 00 01 00 00. Shouldn't 123 be stored as 00 00 00 7b? BinaryWriter uses the little-endian convention to store numbers. This means that the least significant bytes are written first. This is the convention used by Microsoft when developing the .Net framework, which might have been chosen because Intel CPUs natively use the little-endian format. The alternative is big-endian, which stores the most significant bytes first. This corresponds to how we order the digits of numbers. 123 is one-hundred-twenty-three because we assume big-endian notation. If it were little-endian, 123 would represent three-hundred-twenty-one.

Making Sure Resources are Released It is important that we close the writer. As long as we have it open, the file system locks the file, preventing other processes from writing to it. If we forgot to close it, we'd lock ourselves out too. If we clicked the save button twice, we'd fail to open a stream the second time. Instead of manually closing the writer, we can create a using block for it. This defines a scope inside which the writer is valid. Once code execution exits this scope, the writer is disposed of and the stream is closed. using ( BinaryWriter writer = new BinaryWriter(File.Open(path, FileMode.Create)) ) { writer.Write(123); } // writer.Close(); This works because both the writer and file stream classes implement the IDisposable interface. These objects have a Dispose method, which is implicitly invoked when exiting the using scope. The big advantage of using is that it works no matter how execution leaves the scope. Early returns, exceptions, and errors don't break it. It's also concise to write.