In this document we will go over the sample project, the library, and its methods.


Serialization

TST’s Serialization’s routine was written to include as little meta data from the class as possible. This section will go over how to use the serialization methods in TST.


Setting up the class

For a class to work with TST attributes the TinySaveSerializer attribute must be added to the class. This tells the system that this class has properties that can be serialized.

Each property to be included with the serialized data needs the Key attribute. Each key needs a unique order. The order is a byte, so each class can have up to 256 properties. If you need more than that consider refactoring your class structure because more than 256 properties is ridiculous.

All properties to be serialized need to be public and have both a getter and a setter. This is for performance. The following is an example class that can be used with TST:

[TinySaveSerializer]

public class TestClass

{

[Key(Order = 0)]

public int Foo { get; set ; }


[Key(Order = 1)]

public string Bar { get; set ; }

}


Serialization

To serialize the above class the following code would be used:

var test = new TestClass(); test.Foo = 500;

test.Bar = "Testing 1..2..3"

var testBytes = Serialization.Serialize<TestClass>(test);


The result would be an array 25 bytes in length. This array is comprised of:

1 byte for the version of the class

1 byte for the order of property Foo.

1 byte that specifies if the property of Foo has a value or is null 4 bytes for the integer value of 500.

1 byte for the order of property Bar.

1 byte that specifies if the property of Bar has a value or is null 16 bytes for the contents of property Bar.


Deserialization

To desterilize the bytes from the serialization example use the following line:

var testNew = Serialization.Deserialize<TestClass>(testBytes);

An overloaded Deserialize method can also accept a base64 string.

var testNew = Serialization.Deserialize<TestClass>(testBase64String);


Class Version

TST includes a class versioning system. With it you can specify changes to make on your class when it was serialized with an older version of the class or even with a different class name. The class name is not stored inside the serialized data, just the ordering information. All new properties should be given new ordering number after existing ones for upgrading to work.


To use the versioning system you need to use the ITinySaveSerializer interface.

The following example classes show how we can upgrade our earlier TestClass and still be able to deserialize it with the same data.

[TinySaveSerializer(Version = 1)]

public class TestClass2 : ITinySaveSerializer

{

[Key(Order = 0)]

public int Foo { get; set ; }

[Key(Order = 1)]

public string Bar { get; set ; }

[Key(Order = 2)]

public int FooNew { get; set; }


public void OnUpgrade(byte oldVersion, byte newVersion)

{

if (oldVersion == 0 && newVersion == 1)

{

this.FooNew = 1;

}

}

}

The name of the class has been changed to TestClass2 and a new field has been added called FooNew with an Order number of 2.


The method OnUpgrade is called after the data has been deserialized. Inside the method checks the old and new versions to see what it needs to do to upgrade the class. Classes without the Version specified is defaulted to 0. Once the data is reserialized it will have the new structure and class version.


Structs

TST supports structs. Structs do not require any additional tags. All public fields of the struct will be serialized. Properties within a struct are not currently supported.


What objects are supported?

To see a full list of what built in objects are supported look at the MonsterClass example in the reference section of this document.

An example struct called SomeVector3 is also referenced.


What Built-in Objects Are Not Supported.

For performance reasons not all C# built-in types are supported. However, if you have purchased this library, and need a built-in object supported that is currently not, or are experiencing a bug with a supported built-in object please email the developer. If possible, and new version can be deployed.


Base64 Helper

The Serialization class contains two static methods Base64Encode and Base64Decode. These are helper methods. Since saving to Unity’s player preferences does not accept a byte array, these are mostly to make saving and loading to the player preferences easier. The Deserialize method also accepts a base64 encoded string.


Understanding the Included Demo Project

image

In this section we will discuss how to use the Tiny Save Toolbox to save and load objects in a unity project. Open the Serialization Demo to start.

On the bottom left side of the screen are buttons to create three different game objects. On the right side of the screen there is a mock UI to demonstrate saving UI elements.


Saving Transforms

The GameObject SaveDemoController contains a reference to the Sphere, Cube, and Cylinder prefabs.

image

Clicking the create buttons calls SpawnItems in the attached script. This instantiates one of the selected objects types into the scene. If their object index is set to zero, it auto assigns one. More information on the object index is below in the Save Scene Base topic.

The Sphere, Cube, and Cylinder prefabs contain a SaveTransform object. SaveTransform inherits from SceneSaveBase, more on the base class later.


image

The Save Transform script is structured like the classes in the examples used to serialize and deserialize the data. Because the transform can not serialized directly, and even if it could, all of the data in the class is not needed, and would make for a large file size. The SaveTransform class wraps just the needed values that are to be saved from the transform object.


Saving UI

image

The SaveUI class works in the same way as the SaveTransform class. All of the inputs and sliders needed from the UI are referenced in the class in order to save and load the values. The values from the UI elements are placed in properties, so that they are saved and loaded during the serialize and deserialize process.


The Scene Save Base

The SceneSaveBase class contains meta information about the object. It contains the object type (square, sphere, cylinder, or UI), and the object index. Objects that are created at run time are given an index when they are created. There object index value should be left at zero to be auto generated. Objects that are created at design time need to be given an index. No two indexes should be the same. This is so that the serialized data gets to the right place.

The SceneSaveMasterController object contains a field called Current Object Index. This should be set to the first index after the design time indexes.


A Technical Look at Saving a Scene

The SceneSaveFile class allows for all objects to be saved in the same file. When the Save File button is clicked on the method Save is called. This is a detailed description of how the data from the scene is saved. This will give anyone who wants to expand on this example a place to start.


The Save method inside the SaveDemoController class is where the process starts.

public void Save()

{

var fileBytes = this.saveRoot.SaveScene();

this.bytes.text = fileBytes.ToString();

}


The saveRoot object is of type SceneSaveMasterController. It contains a function called SaveScene which returns the number of bytes.

This is the structure of the SceneSaveFile class. The SceneSaveItems property is where all of the scene objects are stored.

[TinySaveSerializer] public class SceneSaveFile

{

[Key(Order = 0)]

public int CurrentObjectIndex { get; set; }


[Key(Order = 1)]

public Dictionary<int , GameObjectHolder> SceneSaveItems { get; set; }

public SceneSaveFile()

{

this.SceneSaveItems = new Dictionary<int, GameObjectHolder>();

}

}


The SaveScene method in the SceneSaveMasterController object looks for all objects in the scene that inherent from the SceneSaveBase class. It stores all of them inside a dictionary of type GameObjectHolder. The GameObjectHolder holds the serialized information of each object on the screen.

public int SaveScene()

{

var file = new SceneSaveFile(); file.CurrentObjectIndex = this.currentObjectIndex;

var saveItems = GameObject.FindObjectsOfType<SceneSaveBase>();


foreach(var item in saveItems)

{

if (file.SceneSaveItems.ContainsKey(item.ObjectIndex))

{

}

else

{


}

}


file.SceneSaveItems[item.ObjectIndex] = new GameObjectHolder(item);

file.SceneSaveItems.Add(item.ObjectIndex, new GameObjectHolder(item));


return SaveObject<SceneSaveFile>( file,

SceneManager.GetActiveScene().name);

}


This is the GameObjectHolder class. It serializes each SceneSaveBase object on initialization, and stores the information needed to put it back the way it was in the scene in the ItemType and ObjectIndex properties.

[TinySaveSerializer]

public class GameObjectHolder : IObjectIndex

{

[Key(Order = 0)] public int ObjectIndex

{

get ; set;

}

[Key(Order = 1)]

public ItemObjectType ItemType { get; set; }

[Key(Order = 2)]

public byte[] ItemData { get; set; }

public GameObjectHolder()

{

}


public GameObjectHolder(SceneSaveBase item)

{

this.ObjectIndex = item.ObjectIndex; this .ItemType = item.ItemType;

this.ItemData = Serialization.Serialize(item);

}

}

The final step is the SaveObject method in SceneSaveMasterController.

Based on the parameters it will serialize, compress, and / or encrypt the data. The data is then stored in the

PlayerPrefs Unity object. More information on Encryption and Compression can be found in later topics.


A Technical Look at Loading the Screen

When the load button is clicked on it calls the Load method inside the SaveDemoController. public void Load()

{

this.saveRoot.LoadScene(this.ItemTypePrefap);

}


This calls LoadScene method inside the SceneSaveMasterController. This method handles loading the information from the LoadObject method into the scene. It will either remove items from the scene that are not in the file, add objects to the scene that are in the file but not in the scene, and update the information for objects that are both in the scene and the file. This is faster then recreating the entire scene and preserves and game object IDs.

public void LoadScene(Dictionary<ItemObjectType, GameObject> itemPrefabMap)

{

//Get all of the items in the scene

var currentItems = GameObject.FindObjectsOfType<SceneSaveBase>();

var currentItemLookUp = new Dictionary<int, SceneSaveBase>(); var file = LoadObject<SceneSaveFile>(

SceneManager.GetActiveScene().name); this.currentObjectIndex = file.CurrentObjectIndex;

// Add all of the current items in an easy to look up table
foreach (var item in currentItems)

{

var tiny = item.GetComponent<SceneSaveBase>();

if (currentItemLookUp.ContainsKey(tiny.ObjectIndex))

{

}

else

{

}

throw new Exception("Item already exists");

currentItemLookUp.Add(tiny.ObjectIndex, tiny);


// Is this item in the save file at all

if (file.SceneSaveItems.ContainsKey(tiny.ObjectIndex) == false)

{

// It was not saved in this file, but it is here now, so delete it GameObject.Destroy(tiny.gameObject);

}

}

//Now go though the file and either load the current values, or create a new object. foreach(var item in file.SceneSaveItems)

{

SceneSaveBase gameItem = null; // Item in current scene

if (currentItemLookUp.TryGetValue(item.Key, out gameItem) == false)

{

var prefab = itemPrefabMap[item.Value.ItemType];

gameItem = Instantiate(prefab).GetComponent<SceneSaveBase>();

}

}

}


The LoadObject method loads the base64 string from the PlayerPrefs. It then deserializes and uncompresses and decrypts if need be. It then returns this to the caller.

internal static T LoadObject<T>(string key, bool compress = true, ICryptoKeys keys = null)

{

var saveString = PlayerPrefs.GetString(key);

byte[] fileBytes = Serialization.Base64Decode(saveString);


object obj = null;

if (keys != null && compress)

{

}

else

{

obj = Encryption.Instance.DecryptWithCompression<T>(fileBytes, keys);

if (keys != null)

{

}

else

{

fileBytes = Encryption.Instance.Encrypt(fileBytes, keys);

if (compress)

{

fileBytes = Compression.Decompress(fileBytes);

}

obj = Serialization.Deserialize<T>(fileBytes);

}

}


if (obj == null)

{

}

else

{


}

}

return default(T);


return (T)obj;


Compression

Use the compression in the case that the sterilized data result is too large. This should be used when the resulting byte array is being sent over the internet or stored on disk.


Compression Examples.

To compress data simply call Compression.Compress(Your Byte Array)

The code test below creates an array of 2,550,000 bytes (2491 kilo bytes). It then compresses it to a byte array of 19,051 bytes (19 kilo bytes)

The rate of compression is dependent on the bytes being compressed. This data is easy to compress because of the pattern in the bytes.

public void TestCompression()

{

var largeByteArray = new List<byte>();

for(int x = 0; x < 10000; x++)

{

for (byte y = 0; y < 255; y++)

{

largeByteArray.Add(y);

}

}


var compressedBytes = Compression.Compress(largeByteArray.ToArray());

}

In the code test example below, the bytes that are created are completely random. The result is very little, if any, compression.

public void TestCompressionRandom()

{

var largeRandomByteArray = new List<byte>();

for (int x = 0; x < 10000; x++)

{

largeRandomByteArray.AddRange(Guid.NewGuid().ToByteArray());

}


var compressedBytes = Compression.Compress(largeRandomByteArray.ToArray());

}


Simple Decompression Example.

To decompress the bytes, use the Compression.Decompress method with the compressed bytes as the parameter. If the bytes in the parameter were not compressed with the Compress method an exception will be raised.

var compressedBytes = Compression.Compress(largeByteArray.ToArray());

var uncompressed = Compression.Decompress(compressedBytes);


Encryption

TST includes two forms of encryption. The first is a single key AES based encryption. The second is a private / public key RSA based encryption.


AES Encryption

AES encryption is a single key encryption algorithm. It is best used when encrypting to a file system or a database.

Setting up AES.

Before AES can be used encryption, keys need to be generated. This is done using the CryptoKeys object . The same keys need to be used to decrypt the data, so they need to be kept in an accessible, but safe location within your system. Ideally, it should not be set within the application doing the encryption, but an outside value that will remain constant.

In this example a GUID are used for the key and iv parameters of the CryptoKeys object

Guid key = Guid.NewGuid(); Guid iv = Guid.NewGuid();

var keys = new CryptoKeys(key, iv);


Simple Encryption Example.

Once the keys have been established. They can be used to encrypt the data.

var bytes = new byte [] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; var encBytes = Encryption.Instance.Encrypt(bytes, keys);

The bytes in this example are hard coded. Normally this would be the bytes from a serialization process.

Simple Decryption Example.

To decrypt the data, using the same keys use the Decrypt method.

var decBytes = Encryption.Instance.Decrypt(encBytes, keys);


RSA Encryption

RSA Encryption is a public / private key encryption algorithm. RSA is normally used when a client and server needs to communicate in a secure way. Each party generates a private and public key. The public key is used for encrypting data but can not be used to decrypt. The private key is used to decrypt the data that was encrypted with the public key. In this way the messages can be sent privately between the two parties. A third party would need the public and private keys of both in order to read the conversation.

RSA example between a client and server.

All encryption and decryption is handled by the RsaCommunication class. When an RsaCommunication object is created it generates new private and public keys.

var client = new RsaCommunication();


Use the GetPublicKey method of the RsaCommunication object to get the public key to send the remote device.

var clientPublicKey = local.GetPublicKey();

The client would then send out their local public key to the server. For most communication this is the only message sent between the client and server that is not encrypted.


The server will also create an instance of RsaCommunication to generate its private and public keys. Once the server has the public key of the client it will call the SetRemotePublicKeyInfo method to set it for its RsaCommunication object.

var server = new RsaCommunication(); server.SetRemotePublicKeyInfo(clientPublicKey);

The server then uses the public key received from the client, to encrypt it’s public key to be sent to the client.

The server now has both a private and public key and can use the EncryptBytes method to encrypt.

var serverEncKey = server.EncryptBytes(server.GetPublicKey());

The DecryptBytes method is used to decrypt data encrypted using the EncryptBytes method.

var serverUnEncKey = client.DecryptBytes(serverEncKey);


The client can now set the public key of the server using the SetRemotePublicKeyInfo method.

local.SetRemotePublicKeyInfo(serverUnEncKey);


Now the client and the server have both their own private keys and the remote location’s public key. This process of exchanging keys is called the “handshake phase”. Once this is complete each side can communicate with the other securely.


This example is used for the client to send information to the server.

var hello = client.EncryptBytes(Encoding.ASCII.GetBytes("Hello world"));


The server can now decrypt the message, and reply to it.

var helloOnServer = Encoding.ASCII.GetString(server.DecryptBytes(hello));

var helloToClientFromServer = server.EncryptBytes(Encoding.ASCII.GetBytes("Hello Client"));

The client would then decrypt the message from the server.

var helloOnClient = Encoding.ASCII.GetString(client.DecryptBytes(helloToClientFromServer));


Reference

The following MonsterClass is an test class that can be used to test TST’s serialization abilities.

using System;

using System.Collections.Generic;

using Com.RJS3Software.TinySaveToolbox.Serialization;


public enum SomeEnum

{

SomeValue1, SomeValue2, SomeValue3, SomeValue4

}


[TinySaveSerializer] public class MonsterClass

{

[Key(Order = 0)]

public bool SomeBool { get; set; }

[Key(Order = 1)]

public byte SomeByte { get; set; }


[Key(Order = 2)]

public sbyte SomeSByte { get; set; }


[Key(Order = 3)]

public char SomeChar { get; set; }


[Key(Order = 4)]

public decimal SomeDecimal { get; set; }


[Key(Order = 5)]

public double SomeDouble { get; set; }


[Key(Order = 6)]

public float SomeSingle { get; set; }

[Key(Order = 7)]

public int SomeInt32 { get; set; }


[Key(Order = 8)]

public uint SomeUInt32 { get; set; }

[Key(Order = 9)]

public long SomeInt64 { get; set; }

[Key(Order = 10)]

public ulong SomeUInt64 { get; set; }

[Key(Order = 11)]

public short SomeInt16 { get; set; }

[Key(Order = 12)]

public ushort SomeUInt16 { get; set; }

[Key(Order = 13)]

public string SomeString { get; set; }


[Key(Order = 14)]

public SomeEnum TheEnum { get; set; }


[Key(Order = 15)]

public SomeClass AClass { get; set; }


[Key(Order = 16)]

public SomeVector3 AVector { get; set; }


[Key(Order = 17)]

public DateTime SomeDateTime { get; set; }

[Key(Order = 18)]

public DateTimeOffset SomeDateTimeOffset { get ; set; }

[Key(Order = 19)]

public bool[] SomeBoolArray { get; set; }

[Key(Order = 20)]

public byte[] SomeByteArray { get; set; }

[Key(Order = 21)]

public sbyte[] SomeSByteArray { get; set; }

[Key(Order = 22)]

public char[] SomeCharArray { get; set; }

[Key(Order = 23)]

public decimal[] SomeDecimalArray { get; set; }


[Key(Order = 24)]

public double[] SomeDoubleArray { get; set; }


[Key(Order = 25)]

public float[] SomeSingleArray { get; set; }


[Key(Order = 26)]

public int[] SomeInt32Array { get; set; }


[Key(Order = 27)]

public uint[] SomeUInt32Array { get; set; }


[Key(Order = 28)]

public long[] SomeInt64Array { get; set; }

[Key(Order = 29)]

public ulong[] SomeUInt64Array { get; set; }


[Key(Order = 30)]

public short[] SomeInt16Array { get; set; }

[Key(Order = 31)]

public ushort[] SomeUInt16Array { get; set; }

[Key(Order = 32)]

public string[] SomeStringArray { get; set; }

[Key(Order = 33)]

public SomeEnum[] TheEnumArray { get; set; }

[Key(Order = 34)]

public SomeClass[] AClassArray { get; set; }

[Key(Order = 35)]

public SomeVector3[] AVectorArray { get; set; }


[Key(Order = 36)]

public List<bool> SomeBoolList { get; set; }


[Key(Order = 37)]

public List<byte> SomeByteList { get; set; }


[Key(Order = 38)]

public List<sbyte> SomeSByteList { get; set; }


[Key(Order = 39)]

public List<char> SomeCharList { get; set; }

[Key(Order = 40)]

public List<decimal > SomeDecimalList { get; set; }

[Key(Order = 41)]

public List<double> SomeDoubleList { get; set; }

[Key(Order = 42)]

public List<float> SomeSingleList { get; set; }

[Key(Order = 43)]

public List<int> SomeInt32List { get; set; }

[Key(Order = 44)]

public List<uint> SomeUInt32List { get; set; }

[Key(Order = 45)]

public List<long> SomeInt64List { get; set; }


[Key(Order = 46)]

public List<ulong> SomeUInt64List { get; set; }


[Key(Order = 47)]

public List<short> SomeInt16List { get; set; }


[Key(Order = 48)]

public List<ushort> SomeUInt16List { get; set; }


[Key(Order = 49)]

public List<string> SomeStringList { get; set; }


[Key(Order = 50)]

public List<SomeEnum> TheEnumList { get ; set; }

[Key(Order = 51)]

public List<SomeClass> AClassList { get ; set; }


[Key(Order = 52)]

public List<SomeVector3> AVectorList { get ; set; }

[Key(Order = 53)]

public Dictionary<int, bool> SomeBoolDictionaryInt { get; set; }

[Key(Order = 54)]

public Dictionary<int, byte> SomeByteDictionaryInt { get; set; }

[Key(Order = 55)]

public Dictionary<int, sbyte> SomeSByteDictionaryInt { get; set; }

[Key(Order = 56)]

public Dictionary<int, char> SomeCharDictionaryInt { get; set; }

[Key(Order = 57)]

public Dictionary<int, decimal> SomeDecimalDictionaryInt { get ; set; }


[Key(Order = 58)]

public Dictionary<int, double> SomeDoubleDictionaryInt { get; set; }


[Key(Order = 59)]

public Dictionary<int, float> SomeSingleDictionaryInt { get; set; }


[Key(Order = 60)]

public Dictionary<int, int> SomeInt32DictionaryInt { get; set; }


[Key(Order = 61)]

public Dictionary<int, uint> SomeUInt32DictionaryInt { get; set; }

[Key(Order = 62)]

public Dictionary<int, long> SomeInt64DictionaryInt { get; set; }

[Key(Order = 63)]

public Dictionary<int, ulong> SomeUInt64DictionaryInt { get; set; }

[Key(Order = 64)]

public Dictionary<int, short> SomeInt16DictionaryInt { get; set; }

[Key(Order = 65)]

public Dictionary<int, ushort> SomeUInt16DictionaryInt { get; set; }

[Key(Order = 66)]

public Dictionary<int, string> SomeStringDictionaryInt { get; set; }

[Key(Order = 67)]

public Dictionary<int , SomeEnum> TheEnumDictionaryInt { get; set ; }


[Key(Order = 68)]

public Dictionary<int , SomeClass> AClassDictionaryInt { get; set ; }


[Key(Order = 69)]

public Dictionary<int , SomeVector3> AVectorDictionaryInt { get; set ; }


[Key(Order = 70)]

public Dictionary<string, bool> SomeBoolDictionaryString { get ; set; }


[Key(Order = 71)]

public Dictionary<string, byte> SomeByteDictionaryString { get ; set; }


[Key(Order = 72)]

public Dictionary<string, sbyte> SomeSByteDictionaryString { get ; set; }

[Key(Order = 73)]

public Dictionary<string, char> SomeCharDictionaryString { get ; set; }


[Key(Order = 74)]

public Dictionary<string, decimal> SomeDecimalDictionaryString { get ; set; }

[Key(Order = 75)]

public Dictionary<string, double> SomeDoubleDictionaryString { get ; set; }

[Key(Order = 76)]

public Dictionary<string, float> SomeSingleDictionaryString { get ; set; }

[Key(Order = 77)]

public Dictionary<string, int> SomeInt32DictionaryString { get ; set; }

[Key(Order = 78)]

public Dictionary<string, uint> SomeUInt32DictionaryString { get ; set; }

[Key(Order = 79)]

public Dictionary<string, long> SomeInt64DictionaryString { get ; set; }


[Key(Order = 80)]

public Dictionary<string, ulong> SomeUInt64DictionaryString { get ; set; }


[Key(Order = 81)]

public Dictionary<string, short> SomeInt16DictionaryString { get ; set; }


[Key(Order = 82)]

public Dictionary<string, ushort> SomeUInt16DictionaryString { get ; set; }


[Key(Order = 83)]

public Dictionary<string, string> SomeStringDictionaryString { get ; set; }

[Key(Order = 84)]

public Dictionary<string , SomeEnum> TheEnumDictionaryString { get; set ; }

[Key(Order = 85)]

public Dictionary<string , SomeClass> AClassDictionaryString { get; set ; }

[Key(Order = 86)]

public Dictionary<string , SomeVector3> AVectorDictionaryString { get; set ; }

}


[TinySaveSerializer] public class SomeClass

{

[Key(Order = 0)]

public bool SomeBool { get; set; }


[Key(Order = 1)]

public byte SomeByte { get; set; }


[Key(Order = 2)]

public sbyte SomeSByte { get; set; }

[Key(Order = 3)]

public char SomeChar { get; set; }

[Key(Order = 4)]

public decimal SomeDecimal { get; set; }

[Key(Order = 5)]

public double SomeDouble { get; set; }

[Key(Order = 6)]

public float SomeSingle { get; set; }


[Key(Order = 7)]

public int SomeInt32 { get; set; }

[Key(Order = 8)]

public uint SomeUInt32 { get; set; }

[Key(Order = 9)]

public long SomeInt64 { get; set; }


[Key(Order = 10)]

public ulong SomeUInt64 { get; set; }


[Key(Order = 11)]

public short SomeInt16 { get; set; }


[Key(Order = 12)]

public ushort SomeUInt16 { get; set; }


[Key(Order = 13)]

public string SomeString { get; set; }

[Key(Order = 14)]

public SomeEnum TheEnum { get; set; }

}


public struct SomeVector3

{

public readonly float X; public readonly float Y; public readonly float Z;


public SomeVector3(float x, float y, float z)

{

this .X = x; this.Y = y; this.Z = z;

}

}