Related Posts:
Part I: @therajmahal/developing-games-with-unity-serialization-part-i
Part II: @therajmahal/developing-games-with-unity-serialization-part-ii
Part III: @therajmahal/developing-games-with-unity-serialization-part-iii
So at the end of Part III we had a Unity project with Full Serializer imported and the skeleton code for our singleton-based Serialization Manager. For a recap, this is what it looks like:
using System;
public sealed class SerializationManager
{
private static volatile SerializationManager m_Instance;
private static object m_syncRoot = new Object();
private SerializationManager()
{
}
public static SerializationManager Instance
{
get
{
if (m_Instance == null)
{
lock (m_syncRoot)
{
if (m_Instance == null)
{
m_Instance = new SerializationManager();
}
}
}
return m_Instance;
}
}
}
And in the actual SerializationManager.cs file in Visual Studio:
So what should we do next?
How to be a good software engineer is very subjective. Some software engineers are incredibly good by jumping right into code, trying something, failing, modifying what they wrote the first time and trying again, failing so on. Others are incredibly good when they plan first. Others are at their best when implementing a hybrid approach and determining how they should continue on a contextual basis.
Since this is a tutorial and I am a big fan of systematic progress, let us think about this for a moment. What exactly are we trying to do in general? Well, we are going to read and write data. Okay, so if at the end of the day we are reading and writing data, what are some things we are going to have to do and things we might want to consider. Here's a list:
- We are going to have to check if the directory we want to save or load data from already exists or not, and how we are going to handle that situation
- We are going to have to check if the file we want to safe or load data from already exists or not, and how we are going to handle that situation
- We may want to be able to specify the encoding on text-based files, with a default encoding if none is specified
- We may want to have use default filenames and extensions, with the ability for them to also be set
Looking at the list, we probably want to address the folder existence issue first, then the file existence issue and then the actual questions about naming and encoding. This post will deal with the first two issues on the list.
Note The list is not an exhaustive one, but hopefully those four items might encourage you to start thinking about what things we might have to consider or address moving forward.
Writing a Check Directory Function
In Part II, we introduced you to the concept of namespaces. We are already using a namespace, the System namespace, because we have a variable of type System.Object (m_syncRoot), which we are using to lock a code block while a thread is executing that code block so that no other threads can go into that code block when its being used.
We are going to have to use an additional namespace, the System.IO namespace, for input/output classes and functions. So at the top of our SerializationManager.cs file, we will add the following line:
using System.IO;
So what does System.IO give us access to? It gives us access to, very conveniently, a Directory class that provides information and functions related to directories. The directory class contains two static functions that we will use:
- Directory.Exists(String strPath), which returns either true or false if the directory exists
- Directory.CreateDirectory(String strPath), which will attempt to create a directory defined by the path string
In pseudocode, we would say:
If the directory does not exist, create it.
Here's the code to do that, in our CheckDirectory function:
public void CheckDirectory(String strPath)
{
if (!Directory.Exists(strPath))
{
Directory.CreateDirectory(strPath);
}
}
That's basically it. Or is it?
What if strPath is an empty string or is null? There are various different ways we could handle this. We could add what we call asserts, which asserts if a condition is not what we expect. We are not going to go that path in this tutorial, although it would be a good idea to learn about them and get used to using them. So lets add that check around the existing conditional (if) statement above:
public void CheckDirectory(String strPath)
{
if (!String.IsNullOrEmpty(strPath))
{
if (!Directory.Exists(strPath))
{
Directory.CreateDirectory(strPath);
}
}
}
So what is that ! (Exclamation Point) before String.IsNullOrEmpty(...) and Directory.Exists(...) function?
The ! operator can be interpreted as "not". So we are testing to see if the string is NOT null or empty and if the directory does NOT exists. Under those conditions we create the directory. Otherwise, we go on our merry way.
Providing Feedback - the Debug Class
Since we are not going down the Assert route in these tutorials, the least we can do is send a message to the console window when we encounter a situation we didn't expect. Unity has a class called Debug for this that has various logging functions, some of which are:
- Debug.Log
- Debug.LogError
- Debug.LogWarning
- Debug.LogFormat
- Debug.LogErrorFormat
- Debug.LogWarningFormat
I never use the first three, but I see it in code from Asset Store packages and other developer's code all the time. Its a matter of preference, but I prefer the latter three and here's why; they accept tokenized strings.
Wait, What is a Tokenized String?
Put simply, its a string that can have tokens in it. That might not have made this any clearer, so I'll provide an example of a non-tokenized string and a tokenized string:
Non-Tokenized
Debug.Log("This is a non-tokenized string");Tokenized
Debug.LogFormat("This is a {0} string", strStringType);
Where strStringType is a string that would have some type of value in it (in this case it would most likely be something like "tokenized".
So, the {0} is a token that is replaced with whatever value is in strStringType. In C#, you can have any number of tokens in a string. For example:
Debug.LogFormat("The {0} {1} {2} {3} {4} the {5} {6}, "quick", "brown", "fox", "jumped", "over", "lazy", "dog");
would print to the console window: The quick brown fox jumped over the lazy dog
You can also use the same token more than once:
Debug.LogFormat("{0}, {0}, {0} your {1}...", "row", "boat");
would print to the console window: row, row, row your boat...
You can achieve the same goal with either types of Debug.Log(...) functions, I just prefer using tokenized strings rather than the concatenation operation (+), which we are not going to cover.
But in the end, its up to you, you are the coder and its your journey, so pick whatever you prefer. In this tutorial we will used tokenized strings.
Alrighty, back to our CheckDirectory function:
public void CheckDirectory(String strPath)
{
if (!String.IsNullOrEmpty(strPath))
{
if (!Directory.Exists(strPath))
{
Directory.CreateDirectory(strPath);
}
else
{
Debug.LogErrorFormat("Directory {0} already exists.", strPath);
}
}
else
{
Debug.LogErrorFormat("Path {0} is null or empty", strPath);
}
}
The Debug class is part of the Unity API, so we are going to have to add the UnityEngine namespace at the top of this file with our other two namespaces (System and System.IO):
using UnityEngine;
However, when we do this, we have a naming conflict. The UnityEngine namespace and the System namespace both have a class called object, so now our object (m_syncRoot) is ambiguous. Is it a UnityEngine.object or is it a System.object? The compiler doesn't know, so now we have to explicitly declare it as one or the other. In this case, we want it to be a System.object, so we will update the line to the following:
private static object m_syncRoot = new System.Object();
And there we have it, we have our CheckDirectory function.
Writing a CheckFile Function
The CheckFile function will be a slightly different than the CheckDirectory function. In the CheckDirectory function we had one string representing the path of the directory. For the CheckFile function we are going to have two strings, one for the path and one for the filename (including extension for this function). The System.IO namespace that we have included gives us access to two additional classes related to files that we are going to use, FileStream and File. FileStream represents the actual stream from the disk to memory, and File contains both data and methods for the file we will be working with.
private void CheckFile(String strPath, String strFilename)
{
if (!String.IsNullOrEmpty(strPath))
{
if (!String.IsNullOrEmpty(strFilename))
{
if (!File.Exists(strPath + strFilename))
{
FileStream fs = File.Create(strPath + strFilename);
fs.Close();
}
else
{
Debug.LogFormat("The file {0}{1} already exists.", strPath, strFilename);
}
}
else
{
Debug.LogErrorFormat("The filename is null or empty.");
}
}
else
{
Debug.LogErrorFormat("The path is null or empty.");
}
}
When we create a File, we also create a FileStream. It is important to close your FileStream after you are finished working with the file. In this case, after we have created the file we are finished. Simple as that!
Alrighty, we'll stop it there. So now we have a Serialization Manager that will create a directory if it doesn't exist already and create a file if it doesn't exist already. In then next post we will actually take those files and add the code to read and write to them.
WIth the code we have now, we can do things like this:
SerializationManager.Instance.CheckFolder("C:/Test");
SerializationManager.Instance.CheckFile("C:/Test/test.txt");
from anywhere else in our codebase. If the folder doesn't exist, it will be created and if the file doesn't exist the file will be created - empty as it may be for now.
How it looks in our SerializationManager.cs file in Visual Studio:
Yes, we could call the folder check inside of the file check. Maybe we will switch it around in the future, but for now we will keep it separated.
Let me know if you have any questions, comments, suggestions.
Thanks for reading! If you like it, please upvote, comment and/or resteem to help build the game development and technology community here on Steemit.