Published on

Update Your (Unity) Game Using PlayFab Without Releasing a New Version Part-2

Authors
  • avatar
    Name
    Gökhan Doğramacı
    Twitter

Table of Contents

Introduction

Welcome to the second part of the series for updating your game with a remote config!

Here you can find the part-1 of this series: Update Your (Unity) Game Using PlayFab Without Releasing a New Version Part-1


In the first blog post, we explored how why you might need a remote configuration, what are the options for setting it up alongside the suggested PlayFab service, and finally how to use PlayFab in a sample project.

In this follow-up post, we will dive deeper into strategies for creating game configurations in PlayFab, using a custom JSON serializer to parse the config data received from PlayFab, and generating a default configuration file to upload to PlayFab. These techniques will help you maximize flexibility when working with remote game configs, allowing for easier updates and maintenance of your Unity game. Let's get started!

By the way, here's a link for you to join the early beta of my latest game:

tailwind logo

Expand: Idle Space Game

My latest game Expand: Idle Space Game is on early beta! Register and help me to find issues, balance things and improve overall feeling of the game by being the one of the very first testers.

Register for beta

Crafting the Perfect Game Config with PlayFab Strategies

A well-structured game configuration is the backbone of your games because the game changes not only during development but also after releasing it. When you are about to do a change, it becomes much easier to modify a configuration file —especially if it’s stored remotely, rather than compiling and releasing a new version.

It’s not also very trivial to come up with a strategy for your game configuration. While having more granularity in the configuration helps precise adjustments and easier A/B testing, it sometimes makes it cumbersome to define or modify them.

We will still focus on the Title Data similar to part-1 of this series, but consider different strategies:

  1. Single Key-Value Pair: You can define a single key-value pair and define everything in the value section as a JSON.
  2. Key-Value Pairs For Everything: If you have only a few configurations, this is a perfect strategy because it allows you to see everything in the PlayFab console. It also allows you to run multiple A/B tests at the same time by overriding specific values without causing overlaps.
  3. Balanced Approach: You can also consider using a balanced approach where there are only a few key-value pairs with combined settings.

In the end, it all depends on the requirements of your game. Here’s an example from my upcoming game Expand:

Example PlayFab Config

Custom JSON Converter: Your Key to Parsing PlayFab Configs

Why do we need a custom JSON converter?

If you check the example configuration I shared from my game, you will notice the values in the key-value pair are JSON representations of the complex configuration data.

In the part-1 of this series, we used the following approach for parsing the data returning from PlayFab:

var downloadedData = new ContentData
{
    IntValue = int.Parse(result.Data["IntValue"]),
    BoolValue = bool.Parse(result.Data["BoolValue"]),
    StringValue = result.Data["StringValue"],
    ComplexValue = JsonConvert.DeserializeObject<ContentData.ComplexData>(result.Data["ComplexValue"]),
};

Even though it is acceptable for this sample case, for more complex cases it can be tedious and prone to errors when we parse the configuration data this way. Think of adding more parameters with value or complex data types in a nested JSON. You’d need to parse each of these data types by providing the parser classes alongside the names of the properties to access them.

Advantages of a custom JSON converter

Using a custom JSON deserializer helps with this process by:

  • Isolating the complex code for the deserialization of any type of data.
  • Removing the code duplications.
  • Being able to access the full power of custom JSON attributes such as JsonProperty, JsonIgnore, JsonConstructor, etc. to customize the deserialization process for each data class.

Implementing a custom JSON converter

When you request the title data (aka configuration) from PlayFab, it will return a Dictionary<string, string> with all properties and their values.

Value can be value-type data, plain string, or a serialized version of complex data. Our converter should know how to approach these different data types to serialize or deserialize them correctly.

Let’s create a custom converter by implementing JsonConverter class. We will call it SerializedValueConverter because, during the deserialization of the main configuration data, we will deserialize also the serialized values in its properties.

public class SerializedValueConverter : JsonConverter
{
    public override void WriteJson(
        JsonWriter writer,
        object? value,
        JsonSerializer serializer)
    {
    }

    public override object? ReadJson(
        JsonReader reader,
        Type objectType,
        object? existingValue,
        JsonSerializer serializer)
    {
    }

    public override bool CanConvert(Type objectType) => true;
}

A custom JSON converter should implement three methods alongside two optional properties (CanRead and CanWrite):

  • WriteJson: This method will be used while serializing an object into a string. Even though we’ll be focusing on data deserialization in this section, we will be using the serializer in the next section.
  • ReadJson: This method will be used while deserializing the string into an object. We will mostly focus on this method.
  • CanConvert: If you have a custom type that you’d like to use this converter for, you can do a type check in this method to decide on returning true or false. However, it’s also okay to return true for all types to make this converter as reusable as possible.

WriteJson implementation

Implementing a converter is not a trivial task. A fully functioning converter does a lot of things. Luckily, we don't have to implement everything.

For the serialization part, what we need to do is simply go through the properties of the object and serialize them. If the property has a simple type e.g. int, string, bool, etc. we will write its value as is, whereas for complex data types, we will first serialize them and write the resulting string:

public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
{
    // ...
    foreach (var property in properties)
    {
        // ...
        var propertyName =
            property.GetCustomAttribute<JsonPropertyAttribute>()?.PropertyName
            ?? contractResolver?.GetResolvedPropertyName(property.Name)
            ?? property.Name;

        writer.WritePropertyName(propertyName);

        if (isSimpleType)
        {
            var propertyValue = property.GetValue(value);
            if (propertyValue == null)
            {
                writer.WriteNull();
            }
            else
            {
                writer.WriteValue(propertyValue);
            }
        }
        else
        {
            string serializedComplexProperty = JsonConvert.SerializeObject(
                property.GetValue(value),
                serializer.Formatting,
                new JsonSerializerSettings { ContractResolver = contractResolver });

            writer.WriteValue(serializedComplexProperty);
        }
    }
    // ...
}

One thing to note is that we’re calling GetCustomAttribute method on properties to access the specific attributes. There are many attributes you can use with Json.NET, but in this implementation, we are only handling JsonPropertyAttribute for simplicity.

The complete implementation of the converter can be found here.

ReadJson implementation

Among other things, the code below simply deserializes given data depending on its type being a value type, complex data type, or a plain string type while executing the callbacks.

public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
{
    // ...
    foreach (var property in contract.Properties)
    {
        // ...
        if (token != null && token.Type != JTokenType.Null)
        {
            object value;
            // Call the property's converter if present, otherwise deserialize directly.
            if (property.Converter != null && property.Converter.CanRead)
            {
                using (var subReader = token.CreateReader())
                {
                    if (subReader.TokenType == JsonToken.None)
                    {
                        subReader.Read();
                    }

                    value = property.Converter.ReadJson(subReader, property.PropertyType!, property.ValueProvider!.GetValue(targetObj), serializer)!;
                }
            }
            else if (property.PropertyType == typeof(string))
            {
                value = token.ToString();
            }
            else if (property.PropertyType?.IsValueType == true || property.PropertyType?.IsPrimitive == true)
            {
                value = token.ToObject(property.PropertyType)!;
            }
            else
            {
                value = JsonConvert.DeserializeObject(token.ToString(), property.PropertyType!)!;
            }

            property.ValueProvider!.SetValue(targetObj, value);
        }
    }

    // ...
}

The complete implementation of the converter can be found here.

Using the custom JSON converter

After implementing the converter, it is extremely easy to use it. We just need to use JsonConvert.DeserializeObject by providing our converter into it:

PlayFab.PlayFabClientAPI.GetTitleData(
    request,
    result =>
    {
        SetLoadingMessage("Processing the result");

        var jObject = JObject.FromObject(result.Data);
        var downloadedData = JsonConvert.DeserializeObject<ContentData>(jObject.ToString(), new SerializedValueConverter());
        contentView.ShowWithData(downloadedData);
    },
    error => contentView.ShowMessage($"Error:\n {error.ErrorMessage}"));

With the custom converter, we don’t need to modify anything in the deserialization code when we add or remove properties.


Creating and Uploading Default Configurations to PlayFab

At this point, we are able to receive the configuration from PlayFab servers when things go as expected. However, this is not always the case as there can be network errors, no internet availability, server issues, etc.

It’s always a good idea to define a default configuration to rely on, in case something goes wrong. It will be useful also when you want to provide the default configuration to PlayFab.

Config (aka Title Data) format accepted by PlayFab

PlayFab allows us to provide title data in JSON format. However, you cannot provide the JSON as is, if some of the values have a complex data type because the complex data types should be serialized as strings. Here’s an example:

class ComplexData {
    bool Prop { get; set; }
}

class Configuration {
    int Val1 { get; set; }
    bool Val2
    ComplexData ComplexVal { get; set; }
}

For the config class above, you might expect that PlayFab accepts the following JSON to import as title data:

{
  "Val1": 1,
  "Val2": true,
  "ComplexVal": {
    "Prop": true
  }
}

But that’s not the case! In order to upload this data to PlayFab, one needs to format it such as:

{
  "Val1": 1,
  "Val2": true,
  "ComplexVal": "{\"Prop\":true}"
}

This is where our WriteJson method shines. It serializes given complex data values as string, so we don’t need to modify the result manually.

Generating the config

All we have to do is use our custom converter to produce the JSON:

var json = JsonConvert.SerializeObject(
        ContentData.Default,
        Formatting.None,
        new SerializedValueConverter())

But it’s not enough to just generate this. It’d be awesome to verify that the generated config can be read as well. Here’s a script to generate the config and also verify that it’s in the expected format:

public static class ConfigGenerator
{
    private static string GenerateConfig() => JsonConvert.SerializeObject(
        ContentData.Default,
        Formatting.None,
        new SerializedValueConverter());

    [MenuItem("Tools/Read Generated Config")]
    public static void ReadGeneratedConfig()
    {
        var content = GenerateConfig();
        var obj = JsonConvert.DeserializeObject<ContentData>(content, new SerializedValueConverter());
        Debug.Log(obj);
    }

    [MenuItem("Tools/Generate Config")]
    public static void PrintGeneratedConfig()
    {
        var config = GenerateConfig();
        System.IO.File.WriteAllText("./config.json", config);
        Debug.Log(config);
    }
}

This script adds two new options under the Tools menu in Unity:

  • Generate Config: Creates a config.json file under the project’s folder with the default game configuration ready to be uploaded to PlayFab.
  • Read Generated Config: Generates the config and then tries to parse it to verify that the generated config is in the correct format.

Conclusion

You can access the final project here with the custom JSON converter and the config generator:

Creating a custom JSON converter is not an easy task, but considering that it’s something you need to do only once and forget, its benefits definitely worth it. To recap, it allows you to:

  • Isolate the complex code for the deserialization of any type of data.
  • Use the full power of the Json.NET framework with its attributes, callbacks, custom serialization strategies, etc.
  • Focus on the data classes rather than the serialization algorithms, after introducing the converter.
Subscribe to the newsletter