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

 

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.