Serializing Game Settings

Today I began the work of migrating my C# Monogame Game Engine (code named Rogue Squad) from a DirectX/Windows codebase to the Windows 10 Universal Windows Platform.  I expected there to be rather large changes required in the refactoring but thus far I’ve only ran into two. I’ll detail the second minor change and why it matters, at the end.

First I started with a straight forward DataContract to hold the fairly basic settings for the game. The annotations allow the DataContract serializer to easily read/write from file in a type-safe way.

[DataContract]
public class GameSettings : IGameSerializableObject
{
    [DataMember]
    public int GlobalVolume { get; set; }
    [DataMember]
    public int FxVolume { get; set; }
    [DataMember]
    public int MusicVolume { get; set; }
    [DataMember]
    public int SpeechVolume { get; set; }
    [DataMember]
    public int ResolutionH { get; set; }
    [DataMember]
    public int ResolutionW { get; set; }
    [DataMember]
    public bool EnableFullScreen { get; set; }
    [DataMember]
    public bool UseVsync { get; set; }

    public static GameSettings Default =>  new GameSettings{ GlobalVolume=100, FxVolume = 100, MusicVolume=100, SpeechVolume=100, ResolutionH=800, ResolutionW=600, EnableFullScreen = false, UseVsync=false };
}

In the DX/Windows app, the serialization is equally straightforward. We simply create or open the file and stream it in, casting the JSON to our GameSettings.

public class AppSettings
   {
      
       public const string GAME_SETTINGS = "gameSettings.json";
       DataContractJsonSerializer serializer = new DataContractJsonSerializer(typeof(GameSettings));

       public GameSettings LoadSettings()
       {
           if (!File.Exists(GAME_SETTINGS)) return GameSettings.Default;
           
           using (FileStream stream = new FileStream(GAME_SETTINGS, FileMode.Open))
           {
               return (GameSettings)serializer.ReadObject(stream);
           }
       }

       public void SaveSettings(GameSettings settings)
       {
           using (FileStream stream = new FileStream(GAME_SETTINGS, FileMode.Create))
           {
               serializer.WriteObject(stream, settings);
           }
       }       
       
       
   }

Unfortunately, UWP’s sandboxed environment means that any sort of direct file writes are out of the question. This also applies to asset loading. On the one hand, this API style has been around a little while – having made its splash with the Windows Phone 7 and the initial WinRT iteration of the Microsoft App Store – so most issues should be long since resolved. Our main problem is, the ‘all async all the time’ API design doesn’t quite mesh well with the ‘loop it baby’ noticeably non-async nature of most game APIs. While this is changing, as of the time of this writing Monogame 3.6 does not make much use of async APIs. We can’t really fault it though, it started as a re-implementation of the defunct XNA library for the XBOX 360. While its codebase has evolved to support everything from PS4 to Xbox One and the Nintendo Switch, it’s design is decidedly stuck in late 2009. That’s not necessarily a bad thing. If it ain’t broke..

using Windows.Storage;

public class AppSettings
    {
  
        public const string GAME_SETTINGS = "gameSettings.json";
        DataContractJsonSerializer serializer = new DataContractJsonSerializer(typeof(GameSettings));
        StorageFolder localFolder;
        public AppSettings()
        {
            localFolder = ApplicationData.Current.LocalFolder;
        }
        public async Task<GameSettings> LoadSettings()
        {
            if (!File.Exists(GAME_SETTINGS)) return GameSettings.Default;
                       
            var file = await localFolder.GetFileAsync(GAME_SETTINGS);
            using (var stream = await file.OpenStreamForReadAsync())
            {
                return (GameSettings)serializer.ReadObject(stream);
            }
        }

        public async Task SaveSettings(GameSettings settings)
        {
            var fileExist = await localFolder.TryGetItemAsync(GAME_SETTINGS);
            if (fileExist == null)
            {
                await localFolder.CreateFileAsync(GAME_SETTINGS);
            }

            var file = await localFolder.GetFileAsync(GAME_SETTINGS);
            using (var stream = await file.OpenStreamForWriteAsync())
            {
                serializer.WriteObject(stream, settings);
            }                        
        }                       
    }

The new version is fairly straightforward and technically “cross-platform” compatible back to Windows 8. The keys changes are the switch to the StorageFolder and StorageFile abstractions as well as the usage of a variety of Async functions.

On the engine side where you’ll eventually consume these settings you’ll either have to tag your methods as async, wrap them in a Task<T>, or call the dreaded .result() method. I was fortunate enough that this code was only being called from the Options Screen in the UI. I was able to mark the event handlers async and called it a day like so..

private async void Back_Resolution_Selected(object sender, PlayerIndexEventArgs e)
{
    //save res settings
    gameSettings.ResolutionH = Engine.Instance.ScreenHeight;
    gameSettings.ResolutionW = Engine.Instance.ScreenWidth;
    await settings.SaveSettings(gameSettings);
   
}

Over the next few weeks I will be posting key challenges and solutions as I continue porting my engine to UWP. Ultimately, the goal is to get everything running on the Xbox One and pick up development from there. It may be a while…

 

Until next time, cheers!

-Jonathan

 

3 Things I wish I knew about F# before I started that big project

Hey everyone, today I wanted to share some insights I’ve gained while learning F# over the past few weeks/months/years (it’s been an on-again, off-again relationship). I love F# but coming from a land of large C# projects, there are some edge cases where you may find yourself tripped up if you’re relatively new to F#.

Bitshift enumerations are not supported

Coming from C#, more than a few of my enumerations followed the pattern of:

enum myEnum = 
   a = 1 << 0,
   b = 1 << 1,
   c = 1 << 2,
   d = 1 << 3

Unfortunately, using bitshift operators within a union/enumeration declaration in F# is not supported. You can however accomplish the same thing using a manually generated bit field as shown below. Easy, but slightly less maintainable.

Type myEnum = 
   a = 0b00001
   b = 0b00010
   c = 0b00100
   d = 0b01000

Null Refs will still plague you

One of the things that quickly becomes apparent when integrating your F# app into a C# ecosystem is that while F# does have a native NULL type, it will still crash spectacularly when dealing with null C# types. Consider the following:

match SomeObj.Prop with  //nullref
   | condition A -> ...
   | condition B -> ... 

Solution? Wrap your questionable calls to C# objects in a ‘toOption’ call

let toOption = function
   | null -> None
   | object -> Some

then you can use it like so..

let myVal = toOption Someobj.Prop
match myVal with
    | Some -> ...
    | None -> ...

Presto! No more null-ref worries in your elegant F# code. Also, you avoid the need for constant “if x <> null then …”.

Serialization…

One of the great things about F# is the ease of record creation. Dreams of serializing these records and sending them across the wire to your web apis can quickly be shattered when you notice all your JSON objects serialized with @suffixes. What gives? F# record members are treated like C# Fields. Their underlying fieldname is serialized by most default .NET serializers resulting in something like the following..

type myRecord {
   Name : string;
   Age:  int;
   Exp:  int;
}

being serialized as:

{ "Name@":"Gandolf", "Age@":"225", "Exp@":"15" }

Fortunately,there’s JSON.NET to the rescue! No, you won’t have to re-write your serialization logic or anything. Simply add the following attributes to your type and you’re done! The rest will be handled auto-magically.

[<CLIMutable>]
[<JsonObject(MemberSerialization=MemberSerialization.OptOut)>]

That’s all for now folks but stop by in the future as I slowly begin to wake from my stupor and flesh out this blog/site.

Cheers