Статьи

Windows Phone 8: Игры

Меня недавно попросили написать статью для нашей будущей серии из 30 статей об использовании RoaminData в игре для Windows 8. Вот эта статья и ссылка на код.

Каждая игра уникальна и несет в себе уникальный набор игровых данных для сохранения и загрузки в различных точках жизненного цикла игры. В прошлом большинство геймеров — особенно казуальные игроки — имели только одно устройство, на котором была установлена ​​их любимая игра. Хранителям данных для этих игр было проще для разработчиков управлять тем, что игровые данные можно было сохранять локально в различных форматах и ​​загружать при запуске игры или по запросу пользователя, и все они содержались в самой игре. Сегодня, когда люди владеют несколькими устройствами как на работе, так и дома, управление играми становится более сложной задачей, поскольку пользователи хотят иметь возможность обмениваться игровыми данными между различными компьютерами без необходимости сохранять эти данные на каком-либо внешнем устройстве, например на флэш-накопителе или портативном устройстве. жесткий диск.Несмотря на то, что существует множество решений для специального кода, которые могут быть разработаны для поддержки обмена игровыми данными между компьютерами, WinRT предоставляет набор классов, которые автоматически облегчают этот тип обмена данными — он называется хранилищем данных роуминга Windows 8. В этой статье мы рассмотрим простую игру, реализованную для WinRT, в которой используется хранилище данных роуминга, которое позволяет игроку остановить игровой процесс на одном компьютере и возобновить его на втором.

Войти в игру

В этой статье мы будем использовать очень простую игру WinRT, основанную на популярной игре Bulls and Cows, часто называемой Mastermind, чтобы проиллюстрировать, как можно сохранять и распространять игровые данные, используя различные функции хранилища данных приложения роуминга в WinRT. ,

Рисунок 1 — Пример игры Mastermind

Геймплей очень прост — компьютер рисует случайный набор из 4 цветных коробок из набора из 6 различных цветов, чтобы сформировать «код». Игрок, часто называемый нарушителем кода », предоставляет ряд« догадок »для решения, выбирая комбинацию из 4 цветов с помощью больших кнопок внизу, которые игровой движок будет проверять на соответствие решению. Игрок получит один красный индикатор для каждого цвета, который выберет игрок, который представляет правильный цвет в правильной позиции в решении, и один белый индикатор для каждого цвета, который выберет игрок, который является частью решения, но НЕ в правильном месте. В приведенном выше примере игрок смог решить игру за 7 попыток, используя дедуктивные рассуждения, основанные на красно-белых индикаторах для каждой из своих догадок. Для нашей игрымы хотим создать среду, в которой пользователь может сохранить свое текущее состояние игры и восстановить его на другом компьютере в будущем. Чтобы понять, как этого добиться, сначала вы должны понять, как работает хранилище данных приложений для роуминга в WinRT.

Хранилище данных приложений в роуминге на WinRT

Хранилище данных приложения для роуминга является частью более крупного хранилища данных приложения WinRT, которое обеспечивает синхронизацию данных между компьютерами, использующими общее приложение. Данные синхронизируются с помощью общей облачной области хранения, прикрепленной к учетной записи Microsoft пользователя.

Рисунок 2 — Синхронизация данных через настройки данных роуминга

Хранилище данных приложений состоит из трех отдельных областей хранения данных:

  • Локальные настройки — данные, которые хранятся и ограничиваются локально в этом единственном экземпляре установленного приложения.
  • Настройки роуминга — данные, которые совместно используются устройствами, на которых пользователь установил определенное приложение.
  • Временные настройки — временные данные, создаваемые приложением, сохраняемые и ограниченные локально на одной машине, которые могут быть удалены ОС в любое время

Каждое из этих трех хранилищ данных можно разбить на файлы приложений и параметры приложения.

Настройки приложения

Настройки приложения хранятся в реестре и представляются разработчику в виде набора простых пар ключ / значение, в которые можно сохранить любой допустимый тип данных WinRT (если необходимы двоичные типы, разработчик должен использовать файлы приложения вместо настроек приложения). Настройки приложения поддерживают концепцию «Контейнеры», в которой наборы связанных настроек приложения могут быть организованы и сохранены вместе. Контейнер по умолчанию используется, когда ни один из них не указан явно, или разработчики могут создавать свои собственные контейнеры глубиной до 32 уровней, чтобы лучше организовать настройки своих приложений. Составные параметры также можно использовать для поддержки атомарных обновлений связанных параметров, чтобы обеспечить согласованность данных при одновременном доступе и роуминге.

Файлы приложений

Интерфейс файлов дает разработчику возможность хранить сериализованные объекты, файлы данных или другие сложные объекты, используя знакомые шаблоны файлов. Под обложками файлы приложений хранятся в файловой системе в скрытой папке на компьютере пользователя, расположенной под установочным каталогом приложения. Файлы могут быть перечислены, созданы, удалены и обновлены с использованием классов в пространстве имен System.IO .

Параметры приложения и файлы приложения автоматически синхронизируются между компьютерами, на которых установлено приложение. Простая запись в настройки приложения или файлы приложения в хранилище данных роуминга позволяет автоматически синхронизировать данные.

Использование хранилища данных приложения роуминга в игре

ПРИМЕЧАНИЕ. Образец кода, который идет с этим сообщением, можно загрузить из CodePlex по следующему URL-адресу:

http://win8roamingstorage.codeplex.com/

Первый шаг к включению данных роуминга состоял в том, чтобы запустить игру со всеми основными функциями и решить, какие биты состояния мне нужно сохранить. Я хотел убедиться, что у меня есть что-то простое для сериализации и десериализации из хранилища, и я также хотел поэкспериментировать как с настройками приложения, так и с файлами приложений, чтобы выяснить, какие из них лучше подходят для различных ситуаций.

Сохранение состояния игры

Поскольку моя игровая логика хранилась в отдельном наборе классов от пользовательского интерфейса приложения, и я использовал шаблон MVVM для управления состоянием приложения для привязки данных, у меня было все необходимое в одном месте — ViewModel . У меня было соблазн просто сохранить всю саму MainViewModel — все в одном куске — но я решил вместо этого сэкономить ее части для экономии места (см. Замечания: Квота ниже ). Моя MainViewModel делает гораздо больше, чем просто удерживает состояние, поэтому я выбрал только те биты, которые были необходимы для сохранения в роуминге. Итак, после каждого хода, представленного игроком, я вызывал следующий метод:

private async void SaveGameState()
{
    StorageHelper.SetGameInProgress();
    await StorageHelper.SaveObjectToRoamingFolder(STR_Gamejson, _game);
    await StorageHelper.SaveObjectToRoamingFolder(STR_Movesjson, Moves);
    StorageHelper.PutObjectToSetting<string>("MoveSlotOne", MoveSlotOne);
    StorageHelper.PutObjectToSetting<string>("MoveSlotTwo", MoveSlotTwo);
    StorageHelper.PutObjectToSetting<string>("MoveSlotThree", MoveSlotThree);
    StorageHelper.PutObjectToSetting<string>("MoveSlotFour", MoveSlotFour);
}

Этот метод использует вспомогательный класс, который я создал для сохранения файлов приложения и настроек приложения в хранилище данных в роуминге.

Сначала мы займемся сохранением файла в папку роуминга. У меня есть два примера в моем исходном коде для этого — первый — это объект Game, который отслеживает текущее состояние игры, включая текущее решение, историю ходов, количество оставшихся ходов и т. Д. Второй — объекты PlayerMoveViewModel, хранящиеся в Moves коллекции. Это обертки вокруг объектов GameMove, которые содержат удобные для пользователя представления данных Move, а также соответствующие MoveResults (т. Е. Количество белых и красных индикаторов). Используемый здесь метод SaveObjectToRoamingFolder в моем классе StorageHelper :

public async static void SaveObjectToRoamingFolder(string filename, object o)
{
    var appData = ApplicationData.Current;
    string jsonData = await JsonConvert.SerializeObjectAsync(o);
    StorageFile sampleFile = await appData.RoamingFolder.CreateFileAsync(filename, CreationCollisionOption.ReplaceExisting);
    await FileIO.WriteTextAsync(sampleFile, jsonData);
}

Я использую Json.NET для своих потребностей в сериализации JSON, так как я использовал его раньше, он быстрый и простой в установке через NuGet

, Используя JSON, я уменьшаю размер каждого хранимого файла до прибл. 1 КБ как для коллекции Moves, так и для самого объекта Game .

Затем мы рассмотрим четыре кнопки, которые игрок использовал для ввода своих догадок. Я все еще чувствую, что сохранение этих настроек не требуется специально с точки зрения удобства использования, но я подумал, что это добавило бы в игру приятный штрих, чтобы доска выглядела точно так, как она была до того, как игрок оставил ее на своей предыдущей машине. Чтобы сохранить эти четыре параметра, я решил использовать хранилище данных параметров приложения, чтобы увидеть, как это сработает. Я расширил свой вспомогательный класс с помощью метода PutObjectToSetting <T> (), чтобы упростить это:

public static void PutObjectToSetting<T>(string key, T value)
{
    var appData = ApplicationData.Current;
    appData.RoamingSettings.Values[key] = value;
}

Короче говоря, этот метод просто присоединяется к текущему объекту RoamingSettings и вставляет значение в соответствии с конкретным ключом, если его нет, или обновляет значение этого ключа, если оно уже существует. Недавно я узнал, что люди называют это «упорством», хотя я не уверен, что я чувствую по поводу этого термина:-)

Загрузка состояния игры

Сохранить состояние игры было действительно легко — после каждого хода игра сохраняла текущее состояние игрового поля и 4 кнопки ввода в Roaming Store. Извлечение данных было немного сложнее, так как я определил 3 варианта использования для загрузки данных:

  • Перезапуск игры на локальной машине
  • Перезапуск игры на вторичном компьютере
  • Обновление текущей запущенной игры на локальном компьютере данными с дополнительного компьютера.

Первые два варианта использования являются основными вариантами использования, которые я определил для этой игры. Я хотел убедиться, что смогу эффективно остановить игру на локальной машине и восстановить ее там, откуда я пришел на удаленной машине. Я также хотел рассмотреть ситуацию, в которой я мог бы приостановить игру на одной машине и поднять ее на другой, чтобы потом вернуться к первой машине. Это было немного больше работы, но в конечном итоге сводилось к некоторому дополнительному коду во время запуска и событию, которое нужно зарегистрировать во время работы приложения.

MVVM является отличным шаблоном для отделения логики пользовательского интерфейса от логики приложения, но иногда — как в случае первоначального запуска приложения, возможны некоторые совпадения. В моем примере приложения используется библиотека MvvmLight Preview от Laurent Bugnion из GalaSoft (также доступная через удобный  пакет NuGet ), чтобы упростить управление. Когда мое приложение запускается, я использую объект MvvmLight Messenger, чтобы отправить уведомление пользовательскому интерфейсу о том, что пользователю нужно спросить, хотят ли они загрузить существующую игру, или запустить новую, когда будет найдено сохраненное состояние игры. Я не хотел делать это в пользовательском интерфейсе, который упростил бы код, поскольку я чувствовал, что он нарушает шаблон MVVM, и я хотел как можно больше придерживаться этого шаблона.

Итак, в конструкторе объекта MainViewModel я регистрирую прослушиватель для GameBoardReadyMessage, чтобы проверить существующее состояние игры и при необходимости предупредить пользовательский интерфейс. В противном случае мы запускаем начало новой игры и позволяем пользователю начать ввод данных.

Messenger.Default.Register<GameBoardReadyMessage>(this, (message) =>
{
    if (StorageHelper.GameInProgress)
    {
        var msg = new AskForGameRestoreMessage();
        Messenger.Default.Send<AskForGameRestoreMessage>(msg);
    }
    else
    {
        StartNewGame();
    }
});

Когда запускается главная страница, она отправляет GameBoardReadyMessage, когда она готова к загрузке, и регистрируется для прослушивания сообщений AskForGameRestoreMessage, поступающих из MainViewModel . Когда запускается AskForGameRestoreMessage, выделенный код в MainPage предложит пользователю подтвердить, что он хочет загрузить существующую игру ( LoadSavedGameMessage ) или запустить новую ( StartNewGameMessage ). В зависимости от ответа вызываются дополнительные сообщения, чтобы предупредить ViewModel о выборе пользователя. Путь, по которому мы хотим следовать, это путь LoadSavedGameMessage в MainViewModel, как реализовано в методе LoadSavedGame :

private async void LoadSavedGame(ApplicationData appData)
{
    try
    {
        CurrentGame = await StorageHelper.GetObjectFromRoamingFolder<Game>(appData, STR_Gamejson);
        CurrentGame.OnFailure += _game_OnFailure;
        CurrentGame.OnVictory += _game_OnVictory;
        Moves.Clear();
        var moves = await StorageHelper.GetObjectFromRoamingFolder<ObservableCollection<PlayerMoveViewModel>&t;(appData, STR_Movesjson);
 
        foreach (var move in moves)
        {
            Moves.Add(move);
        }
 
        MoveSlotOne = StorageHelper.GetObjectFromSetting<string>(appData, "MoveSlotOne");
        MoveSlotTwo = StorageHelper.GetObjectFromSetting<string>(appData, "MoveSlotTwo");
        MoveSlotThree = StorageHelper.GetObjectFromSetting<string>(appData, "MoveSlotThree");
        MoveSlotFour = StorageHelper.GetObjectFromSetting<string>(appData, "MoveSlotFour");
    }
    catch (Exception ex)
    {
        // show error message
        ClearSavedGameState();
        Messenger.Default.Send<ErrorLoadingGameMessage>(new ErrorLoadingGameMessage(ex));
        // start new game
        StartNewGame();
    }
}

В этом примере кода мы пытаемся извлечь все сохраненное состояние игры из файлов приложения и из настроек приложения и воссоздать игру в процессе. Если что-то пойдет не так, мы сообщим пользователю об этом через оповещение и просто запустим новую игру. Методы, которые я добавил в класс StorageHelper, выполняют обратную работу, которую мы проделали, чтобы сохранить объект в хранилище, и вместо этого вытащить данные обратно:

public async static Task<T> GetObjectFromRoamingFolder<T>(ApplicationData appData, string filename)
{
    StorageFile sampleFile = await appData.RoamingFolder.GetFileAsync(filename);
    string jsonData = await FileIO.ReadTextAsync(sampleFile);
    var o = await JsonConvert.DeserializeObjectAsync<T>(jsonData);
    return o;
}

GetObjectFromRoamingFolder <T> берет данные из хранилища роуминга и использует Json.NET, чтобы превратить эти данные обратно в объект по нашему выбору ( обратите внимание, что этот метод должен быть помечен как асинхронный, поскольку базовый доступ к RoamingFolder требует ожидания ).

public static T GetObjectFromSetting<T>(ApplicationData appData, string setting)
{
    var raw = appData.RoamingSettings.Values[setting];
    T obj = (T)raw;
    return obj;
}

Наконец, метод GetObjectFromSetting <T> () извлекает значение из коллекции RoamingSettings и преобразует его в правильный тип перед возвратом.

Другие вещи, чтобы рассмотреть

Есть несколько особенностей, связанных с хранилищем данных роуминга, которые необходимо учитывать, прежде чем использовать его возможности в вашем приложении. Я не учел все из них в моем примере, но я хотел бы перечислить наиболее важные из них, которые вы должны учитывать при внедрении данных роуминга в вашей игре:

квоты

В отличие от данных, хранящихся в области локальных данных, данные роуминга регулируются квотой хранилища.

, как определено свойством RoamingStorageQuota объекта ApplicationData. Эта квота обеспечивает максимальный размер данных, которые можно синхронизировать в облаке через хранилище данных роуминга. На момент написания статьи текущее значение RoamingStoreQuota составляет 100 КБ. Если этот предел превышен, приложение продолжит работу, но данные НЕ будут синхронизированы с облаком для доступа других установок. Для игровых данных это важное соображение при разработке игры. Вам нужно будет оптимизировать объем хранимых данных и используемые форматы сериализации, чтобы гарантировать, что вы останетесь в этом диапазоне или рискуете отключить автоматическую синхронизацию.

Versioning

Игры, как и традиционные приложения, со временем эволюционируют, приобретая новые функции, исправляя ошибки и улучшая игровой процесс. Вам следует продумать стратегию управления версиями данных вашего приложения для роуминга так же, как стратегию управления версиями для самой игры. Класс ApplicationData предоставляет свойство Version, а также метод SetVersionAsync (), чтобы позволить вам установить версию для ваших данных и зарегистрироваться при загрузке данных приложения. Если номер версии сохраненных данных меньше, чем версия, которую ожидает ваше приложение, приложение может либо отклонить сохраненное состояние игры в пользу новой игры, либо вы можете предоставить стратегию обновления для преобразования старых сохраненных данных игры в новый формат. Текущее руководство заключается в использовании последовательных и увеличивающихся номеров версий, начиная с 1, для ваших потребностей в версиях Application и ApplicationData.

Событие DataChanged

В зависимости от того, как ваша игра предназначена для использования, может возникнуть ситуация, когда существует более одного одновременно запущенного экземпляра вашей игры. В этой ситуации данные, синхронизированные через облако, могут отличаться или даже быть старше, чем данные, с которыми вы работаете локально. Это может создать своего рода коллизию данных, когда приложению необходимо будет принять решение о том, как действовать дальше. Чтобы помочь вам справиться с этими коллизиями, хранилище данных роуминга предоставляет простое событие, к которому вы можете подключить обработчик событий для решения этой ситуации — событие DataChanged:

public event TypedEventHandler<ApplicationData, Object> DataChanged

Подписавшись на это событие, ваше приложение будет уведомлено об изменении состояния игры на другом компьютере, и вы сможете ответить соответствующим образом. В моем примере приложения я отвечаю на это событие, вызывая LoadSavedGame , передавая предоставленный объект ApplicationData, чтобы обновить состояние игры и пользовательский интерфейс.

Обратите внимание: событие DataChanged НЕ предназначено для того, чтобы облегчить ходы игрока в пошаговой игре. Не только игровые данные синхронизируются для одной учетной записи Microsoft, но и скорость, с которой происходят обновления, не гарантируется как своевременная для приемлемого игрового процесса. Существуют и другие методы, такие как сокеты, которые обеспечивают гораздо лучшую альтернативу созданию сетевых пошаговых игр и приложений.

Резюме

Это был действительно забавный проект для меня. Я никогда прежде не писал игры, в какой бы то ни было форме, поэтому меня беспокоил этот аспект так же, как и стратегии, которые я бы использовал для загрузки и сохранения состояния игры. Я разместил весь свой код на CodePlex по следующему URL-адресу, чтобы вы могли его скачать и проверить на досуге:

http://win8roamingstorage.codeplex.com/

Пожалуйста, имейте в виду, что это не «полная» игра, поскольку я не думаю, что она готова для Магазина Windows (по крайней мере, пока), но будет интересно продолжить работу над ней, чтобы попасть туда.

Более подробную информацию о настройках приложения для роуминга и объекте ApplicationData можно найти по всему Интернету. Вот список дополнительных статей, ссылок и примеров кода, которые помогут вам начать работу.