C# Object Serialization for GameSparks - Using Reflection

Introduction

The serialization code covered in this tutorial will allow you to send objects containing most data-types to the server by serializing them directly into a form GameSparks can understand. This is recommended when you want to save game-states (for example) to GameSparks and to the client locally. Using a serializer, you can save the same game-state object to disk and to GameSparks, synchronizing the data without having to parse out individual fields and sending them to the server. Simply give the object you want to send to the method and it will create a GSRequestData object from this which can be sent to GameSparks.

This tutorial will cover the basics of how to achieve this but there are types excluded which may require you to develop the serializer further - if you want to add dictionaries for example.

There's also no code for serializing primitives directly because the GameSparks SDK already offers GSRequestData for most data-primitives, so we'd advise you to use that instead.

Serialization Code

The basis for this tutorial is the GameSparksSerializer.cs class. This class will be broken down into two main methods: GSDataToObject and ObjectToGSData. To begin with, create a new C# class called GameSparksSerializer.cs.

ObjectToGSData

The ObjectToGSData method will take a C# object and, using reflection, we'll check what fields exist in that object. It will also check if those fields are serializable, allowing you to mark certain fields in your objects to be sent to GameSparks or not.

Using reflection allows us to gain information about the Object without needing to know which specific fields exist or having to create separate methods for separate object-types.

We'll be converting the information into JSON, so we're going to use field-names as keys for our JSON data. We'll also need to store the object type as a separate field so we can create an object out of our JSON data when we want to get the object back.

We start with this method, because we need to have some data stored correctly on the server before we can retrieve it and convert it back into an object.

GSDataToObject

Our object will be stored in a collection on the server as JSON data. We need to create a method to retrieve this data and parse it back into the original form. We'll therefore be passing GSData from the server response into the GSDataToObject method.

Reflection

Reflection is the ability of a program to inspect its own meta-data at runtime. You could think of this as dynamic code generation at runtime. Instead of hard-coding all the class types we expect to serialize, we can use reflection to instantiate types at runtime based on what the compiler understands about those classes, types, and the fields they contain.

Reflection is quite a broad area of C#, and we're only covering a small portion of it in this tutorial, so we would advise looking into it further if you wish. There is a good explanation of reflection available here.

The main class we'll be using in this tutorial is System.Reflection.FieldInfo. This class allows us to inspect information about fields in our classes. Using this, we can extract only the information we need to serialize for any object we want to send to the server.

Note: There are some limitations for reflection with iOS, though we'll not encounter those limitations in this tutorial. But it's still important to know this if you want to develop this serializer further. There are some notes on this issue here.

ObjectToGSData

Create a new method called ObjectToGSData. We'll make this a static method because there's nothing else that relies on an instance for this method to run. The return type of this method will be GSRequestData.

GSRequestData is essentially a wrapper for the JSON data we are going to send to the server. Importantly, it provides methods for converting objects, enumerables, and primitives to GSData so that GameSparks requests can recognize the format and send it to the server. This will save us writing a lot code ourselves:

publicstatic GSRequestData ObjectToGSData(object obj)
{
}

This method will have several steps:

1. First we create a new GSRequestData object to which we'll add fields and return from this method.

2. For each value we add, we include a new field called type. This will be the type of the object we are converting to GSData (we'll use this later to parse the field back to the correct type).

3. We then iterate through every field we find for that type using the System.Reflection.FieldInfo class. This class gives us many details about the object’s fields. These will be explained as we go through the tutorial.

We want the serialize to ignore fields marked as not serializable. We can do this very easily with the FieldInfo class. Using the field we can get the type of that field and then use field.IsNotSerialized.

2. Check that the value for that field is not null.

In other words, just because the class has a field defined doesn't mean that there is any data for the field. In this case, there's no point sending that field to the server, so we exclude it from the serializer. We can do this using field.GetValue(). This method takes an object, in this case the object we are serializing.

The AddString() method takes the object you want to serialize and a key used to define it. In this case we used the name of the field as the key. This is important because using the field-name allows us to search back through fields when we are parsing back into an object.

Our ObjectToGSData request can now serialize any strings in any objects you want to throw at it. If you want to check what your GSData looks like in JSON form, you can add a log to print out the JSON before returning it:

Testing

So to test this, all we need to do is create a class with a few variables. We'll create a few different types of variables that we'll serialize in the next steps of this tutorial and to prove that this code will currently only serialize strings:

And now, if we try to serialize our test-class again, we should see all our variables listed as JSON.

Setting Fields as Non-Serializable

For those not familiar with serialization in C#, there are some ways to set classes and fields as serializable or non-serializable.

This is important, because there's usually some fields in your class that you want to send to the server and some you do not. There will also be some fields that are not serializable at all using this tutorial, though you can always add those yourself (this will just cover serializing the basics).

In Unity3D, for example, classes like Button or GameObject could have triggers or event-listeners you do not want to send to the server.

Stopping fields from being serialized:

One way to stop fields from being serialized is to set them as private.

Above, you can see that the myString field is marked with this attribute, and the myInt field is marked as private. So we can expect this code to only serialize the float and the bool:

Object - List and Arrays

The GSRequestData class has the ability to create JSON data from two types of lists or arrays out-of-the-box. These can be lists of numbers (float, int, double) or lists of strings. From the point of view of GSRequestData, it only accepts lists, so any arrays we want to send will have to be converted first.

Strings

The first thing we need to do is check that the field type is an array of strings. Then we will use the GSRequestData.AddStringList() method. As mentioned, this only accepts a list of objects, so we'll convert our array to a string-list.

To save space we'll use a conditional statement here so that we can have the method accept a list or array in the same line without if-statements:

Then we need to add this object to our test-class. We won’t review an example of this code, because these examples will begin to take up a lot of space. All we're doing is creating a new field and then instantiating the field in the test-class constructor.

The first thing we need to do in our serializing code is to check if the object is a class. Luckily, reflection in C# has a Boolean on the field type that allows us to check this. All we need to do is to pass the class back to ObjectToGSData() again so we can use the same code to serialize this object to the original GSData variable:

Testing

With our PlayerInfo field in the test-class, we should expect to see the field added to the JSON with the class-type added as it was at the start of this tutorial:

Lists of Objects

Now we have almost everything we need to get the basics of a data-serializer working with the GameSparks SDK. The last type we are going to look at in this example is lists or arrays of objects.

For this example, we are going to create a list of items, such as you might find in a player’s inventory. So, using this example you could serialize the player’s entire inventory and send it to the server to save it. This is a quick and useful way to save data to the server without needing to edit or update items individually:

Back in our serializer code, we're again going to check that we have the correct type. The first thing we need to do is check if we have a generic type. This is because technically our list can be interpreted as a class with the way we have our code written. So we'll first use FieldType.IsGenericType to make sure our class serializer is checking for a class or list.

We're going to use the GetGenericTypeDefinition instead of just FieldType to make sure we have a list and not some other generic-type:

Within this if-statement we'll create a new list of GSData. This is going to be added to our main gsData variable as an object-list (we’ll see this in the JSON data later).

Then we'll iterate through object-list and add new GSData objects for each element in the list using ObjectToGSData() again, as we did when we were saving nested objects.

Something important to note is that we don’t know what type of list we might be serializing, so instead of a list we'll cast our list to IList. This allows us to forget about the type of the list and just iterate through it:

Testing

Now you should be able to run your code and check out what we get from the JSON log:

Arrays of Objects

Lastly, we should add some code to allow arrays to be serialized as well. Since we use the IList, we don’t have to do anything special with arrays, because IList will have them the same once we cast them.

All we need to do is add some code to the object-check to make sure it doesn’t try to serialize arrays (as we made sure it didn’t serialize lists). After that, we add a cast to our object-list check to make it also accept arrays:

NOTE 1: Although this process for saving data to the server is easier, you should also keep in mind the amount of data you are sending to the server. GameSparks limits the size of data that can be send through a request (<256Kb).

NOTE 2. Also keep in mind that although you may not reach this limit, a large amount of data will have a significant effect on the speed of requests and responses.

Sending Data to the Server

The next step is to actually send this serialized JSON to the server and save it in a collection. For this example, we're going to keep it simple - we're going to create an Event where we'll send these data, and we'll insert the data from the client into the collection:

In our save_data event we add the following Cloud Code:

We'll use collection.findAndModify() to search for a player’s ID and then insert the new data.

We'll use the upsert functionality to automatically create a new doc for this player if they don’t have one already:

var playerData = Spark.getData().data;
// find an modify the doc for this player in the 'playerData' collection using their playerId //
// if there is no doc for the player then we use upsert to create one //
var doc = Spark.runtimeCollection('playerData').findAndModify({ "playerId" : Spark.getPlayer().getPlayerId() }, null, {}, false, {
$set : {
'updated' : new Date(),
'data' : playerData
}
}, false, true); // <- upsert is the last optionin this function, set to'true'
// last, return the player data to prove it was updated //
Spark.setScriptData('playerData', doc)

Note! We have a line at the end to return the doc we are saving. This is just for this tutorial, you can remove it later if you like. All it does is to prove the doc was saved correctly for us by returning it back.

Now we just need to put some code in to send our GSData to GameSparks using the log-event we just created.

You can put this code where you like when implementing your own requests. For this example, we use a button listener:

Requesting Data Back

Now that we have the data saved to the server, the first thing we need to do is to get it back. For this we need another log-event request. You can create one through the GameSparks portal Events tab:

The Cloud Code for this Event is going to be simple:

All we need to do is find the document in the collection for this player, and return it.

If there is no document for the player, we will return an error:

// use 'findOne' to get the player doc //
var playerData = Spark.runtimeCollection('playerData').findOne({ 'playerId' : Spark.getPlayer().getPlayerId() });
if(playerData){ // if we have player data in that collection, return it //
Spark.setScriptData('@getPlayerData', playerData.data);
}else{ // if no player data exists, return an error //
Spark.setScriptError('@getPlayerData', 'no-player-data');
}
// I used @ to denote the event name, but you can name the data whatever you like

Now we'll create the request on the client-side to get this information back:

If we run this code now, you should see the same JSON data you sent to the server in the last section again:

Recreating the Object

This method is very similar to ObjectToGSData, only in reverse. The steps are:

1. Parse the type field for this object into a Type.

2. Create an object of that type (this is what is returned from the method).

3. Go through all fields for this object-type and find the matching field-names in our GSData.

4. Convert the GSData to an object using the Get functions.

The first step will be to get the type. This is pretty simple, because we can create a type from a string and we stored the type of each object in a JSON field. The next step will be to use the Activator class to create an instance of an object from that type.

Then we'll iterate through each field, excluding those fields that cannot be serialized:

De-Serializing the Basics (String, Int, Bool, Float)

We'll start with a few basic types to test how this works. The logic works the same as with ObjectToGSData. We just check the field-type and then use the appropriate method. In this case we use GSData.GetNumber(), GetString, and so on.

Once we have the data from the GSData, we need to apply it to our object. We do this with FieldInfo.SetValue(). This is a very useful method that allows you to set the value of fields in your object and, given that we know the field name, we can be sure the right fields get the right values:

Testing

Testing that the data has been de-serialized requires us to add a method to our MyTestClass class. We're calling this method Print(), and we're going to call it just after we cast the object from GSDataToObject() back into MyTestClass.

All we need to do here is print out the values for any fields in the class we are de-serializing:

If the value did not de-serialize properly for some reason, we can expect that value to be null and so we should get a null-reference error.

If the data is de-serialized correctly, we'll expect to see the same value as the JSON we print at the top of the method:

Another thing to note is that constructors for MyTestClass, PlayerInfo, and Item contain setup code already. And at the moment, this code is the same as the GSData we are getting back from the server. So when our de-serialization code creates these objects, it will instantiate them with the same values we expect to get back from the server. Therefore, it is redundant to test unless you remove those constructors:

And there it is! We have our first few primitive types back from the server after converting them to JSON. From here, you should be able to figure the rest of the process out by yourself, but we'll go through the next few steps anyway.

DateTime

DateTime objects are simple to de-serialize, because GSData has a GetDate() method. In order to test this, we'll have to get the DateTime field of our test-class to print out the value for myDate:

GSData - List and Arrays

Because we already covered how to get lists and arrays back, there is no need to go in-depth with it again here. GSData will only return List types, so we'll use a conditional-operator to find the right type (List or Array):

In the image above, all we did to the Print() method was print out the count or length of the array or list to check it was correct. To be doubly-sure, we iterated through the elements and printed those out too.

Nested Objects

As with ObjectToGSData(), nested objects can be de-serialized by passing in the GSData to the GSDataToObject() method again. We'll check that it is a class, and that it is not generic or an array. This will allow us to de-serialize arrays or lists of nested objects next:

List of Nested Objects

This section is separate from arrays unlike in the previous section. This is because we cannot use the Activator class to create arrays with a certain type, while we can for lists. Instead we'll use Array.CreateInstance() to make the array of the objects. We'll then de-serialize the GSData into that array and copy that array into the array of the correct type:

elseif(!typeField.FieldType.IsArray && typeof(IList).IsAssignableFrom(typeField.FieldType))
{
IList genericList = Activator.CreateInstance(typeField.FieldType) as IList;
foreach(GSData gsDataElem in gsData.GetGSDataList(typeField.Name))
{
object elem = GSDataToObject(gsDataElem);
genericList.Add(elem);
}
typeField.SetValue(obj, genericList);
}
elseif(typeField.FieldType.IsArray)
{
List<GSData> gsArrayData = gsData.GetGSDataList(typeField.Name);
// create a new instance of the array. The Activator class cannot do this for arrays //// so this will create a new array of types inside the array, with the count of what is in the gsdata list //Array newArray = Array.CreateInstance(typeField.FieldType.GetElementType(), gsArrayData.Count);
object[] objArray = new object[gsArrayData.Count]; // create a new array of objects where the serialized objects will be keptfor(int i = 0; i < gsArrayData.Count; i++)
{
objArray[i] = GSDataToObject(gsArrayData[i]); // convert the JSON data inside the list to an object
}
Array.Copy(objArray, newArray, objArray.Length); //covert the object[] to the original type
typeField.SetValue(obj, newArray);
}

Conclusion

And this is our GameSparks serializer complete. Once again, there some types missing here that you can now add yourself, because any types will follow the same pattern as we have covered in this tutorial.