«Mango» — внутреннее кодовое название Windows Phone SDK 7.1 и, конечно же, название изысканного тропического фрукта. Плоды манго можно употреблять в пищу самыми разными способами, например добавлять в пироги, салаты, коктейли. Говорят также, что манго очень полезен для здоровья, и у него интересная история. В этой статье я рассмотрю Mangolicious — приложение Windows Phone SDK 7.1, посвященное плодам манго. В приложении содержатся рецепты с использованием манго, составы коктейлей и различные факты о манго, но настоящей целью этого приложения является исследование некоторых из важнейших новых возможностей в выпуске SDK 7.1, а именно:
- локальная база данных и LINQ to SQL;
- вторичные тайлы (secondary tiles) и глубокое связывание (deep linking);
- интеграция Silverlight/XNA.
Пользовательский интерфейс этого приложения прост: основная страницы предлагает панораму с меню в первом элементе панорамы, динамическим выбором рецептов и коктейлей текущего сезона во втором элементе и некоторые сведения о плодах манго в третьем элементе, как показано на рис. 1.
Рис. 1. Основная панорамная страница Mangolicious
И меню, и элементы в разделе Seasonal Highlights действуют как ссылки для перехода на другие страницы приложения. Большая часть страниц является Silverlight-страницами, а одна выделена под интегрированную XNA-игру. Вот сводка задач, которые нужно выполнить для создания этого приложения, — от начала и до конца:
- Создать базовое решение в Visual Studio.
- Независимо создать базу данных для рецептов, коктейлей и интересных фактов.
- Обновить приложение так, чтобы оно использовало эту базу данных и предоставляло доступ к ней для связывания с данными.
- Создать различные UI-страницы и связать с ними данные.
- Установить функцию Secondary Tiles, чтобы пользователи могли прикреплять элементы Recipe к странице Start устройства под управлением Phone.
- Интегрировать в приложение XNA-игру.
Создание решения
Для этого приложения я воспользуюсь шаблоном Windows Phone Silverlight and XNA Application в Visual Studio. Это приведет к генерации решения с тремя проектами; они представлены в табл. 1 после переименования.
Табл. 1. Проекты в решении Windows Phone Silverlight and XNA
Проект | Описание |
MangoApp | Содержит само приложение Phone со страницами по умолчанию: MainPage и вторичной GamePage |
GameLibrary | По большей части пустой проект, в котором содержатся все необходимые ссылки, но нет никакого кода. Очень важно, что он включает Content Reference на проект Content. |
GameContent | Пустой проект Content, который будет содержать все ресурсы игры (изображения, звуковые файлы и т. д.) |
Создание базы данных и класса DataContext
В Windows Phone SDK 7.1 введена поддержка локальных баз данных. То есть приложение может хранить данные в локальном файле базы данных (SDF) на устройстве Phone. Рекомендуемый подход — создать базу данных в коде либо самого приложения, либо отдельной вспомогательной программы, предназначенной только для этой цели. Создавать базу данных в вашем приложении имеет смысл, когда вы будете создавать все данные (или большую часть) только при выполнении этого приложения. В случае приложения Mangolicious у меня есть лишь статические данные, и я могу заполнить базу данных заранее.
Для этого я создам отдельное вспомогательное приложение на основе простого шаблона Windows Phone Application. Чтобы создать базу данных программным способом, нужен класс, производный от DataContext, который определен в Phone-версии сборки System.Data.Linq. Этот класс DataContext можно использовать как во вспомогательном приложении, создающем базу данных, так и в основном приложении, которое просто работает с базой данных. Во вспомогательном приложении я должен указать размещение базы данных в изолированном хранилище, так как это единственное место, в которое можно что-то записывать из приложения Phone. Этот класс также содержит набор полей Table для каждой таблицы базы данных:
public class MangoDataContext : DataContext
{
public MangoDataContext()
: base("Data Source=isostore:/Mangolicious.sdf") { }
public Table<Recipe> Recipes;
public Table<Fact> Facts;
public Table<Cocktail> Cocktails;
}
Между классами Table и таблицами в базе данных существует сопоставление один в один. Свойства Column сопоставляются с полями (столбцами) таблицы в базе данных и включают такие свойства схемы базы данных, как тип данных и размер (INT, NVARCHAR и др.), может ли поле содержать null-значения, является ли оно полем ключа и т. д. Я определяю классы Table для остальных таблиц в базе данных точно так же, как показано на рис. 2.
Рис. 2. Определение классов Table
[Table]
public class Recipe
{
private int id;
[Column(
IsPrimaryKey = true, IsDbGenerated = true,
DbType = "INT NOT NULL Identity", CanBeNull = false,
AutoSync = AutoSync.OnInsert)]
public int ID
{
get { return id; }
set
{
if (id != value)
{
id = value;
}
}
}
private string name;
[Column(DbType = "NVARCHAR(32)")]
public string Name
{
get { return name; }
set
{
if (name != value)
{
name = value;
}
}
}
... // остальные определения полей (столбцов)
// опущены для краткости
}
Тем не менее во вспомогательном приложении — при использовании стандартного подхода Model-View-ViewModel (MVVM) — мне теперь нужен класс ViewModel, который будет выступать в роли посредника между View (UI) и Model (данными), используя класс DataContext. В ViewModel есть поле DataContext и ряд наборов для табличных данных (Recipes, Facts и Cocktails). Эти данные статичны, поэтому в данном случае будет достаточно простых наборов List<T>. По той же причине мне нужны лишь аксессоры get свойств, но не модификаторы set (рис. 3).
Рис. 3. Определение свойств-наборов для табличных данных в ViewModel
public class MainViewModel
{
private MangoDataContext mangoDb;
private List<Recipe> recipes;
public List<Recipe> Recipes
{
get
{
if (recipes == null)
{
recipes = new List<Recipe>();
}
return recipes;
}
}
... additional table collections omitted for brevity
}
Я также предоставляю открытый метод (который можно вызывать из UI) для реального создания базы данных и всех ее данных. В этом методе я создаю саму базу данных, если ее нет, а затем создаю каждую таблицу по очереди, заполняя их статическими данными. Например, чтобы создать таблицу Recipe, я создаю несколько экземпляров класса Recipe, соответствующих записям в таблице, добавляю все записи в наборе к DataContext, а затем фиксирую данные в базе данных. Тот же шаблон используется для таблиц Facts и Cocktails (рис. 4).
Рис. 4. Создание базы данных
public void CreateDatabase()
{
mangoDb = new MangoDataContext();
if (!mangoDb.DatabaseExists())
{
mangoDb.CreateDatabase();
CreateRecipes();
CreateFacts();
CreateCocktails();
}
}
private void CreateRecipes()
{
Recipes.Add(new Recipe
{
ID = 1,
Name = "key mango pie",
Photo = "Images/Recipes/MangoPie.jpg",
Ingredients = "2 cans sweetened condensed milk, ¾ cup fresh key lime juice, ¼ cup mango purée, 2 eggs, ¾ cup chopped mango.",
Instructions = "Mix graham cracker crumbs, sugar and butter until well distributed. Press into a 9-inch pie pan. Bake for 20 minutes. Make filling by whisking condensed milk, lime juice, mango purée and egg together until blended well. Stir in fresh mango. Pour filling into cooled crust and bake for 15 minutes.",
Season = "summer"
});
... additional Recipe instances omitted for brevity
mangoDb.Recipes.InsertAllOnSubmit<Recipe>(Recipes);
mangoDb.SubmitChanges();
}
После этого в подходящем месте вспомогательного приложения (например, в обработчике щелчка кнопки) я могу вызвать этот метод CreateDatabase. Когда я запускаю вспомогательное приложение (либо в эмуляторе, либо на физическом устройстве), в изолированном хранилище приложения создается файл базы данных. Последняя задача — извлечь этот файл в настольную систему, чтобы им можно было пользоваться в основном приложении. Для этого я использую утилиту командной строки Isolated Storage Explorer, поставляемую с Windows Phone SDK 7.1. Вот команда, которая делает снимок изолированного хранилища из эмулятора и передает его в настольную систему:
"C:\Program Files\Microsoft SDKs\Windows Phone\v7.1\Tools\IsolatedStorageExplorerTool\ISETool" ts xd {e0e7e3d7-c24b-498e-b88d-d7c2d4077a3b} C:\Temp\IsoDump
В этой команде предполагается, что данная утилита установлена по стандартному пути. Параметры поясняются в табл. 2.
Табл. 2. Параметры командной строки Isolated Storage Explorer
Параметр | Описание |
ts | Сокращение от «take snapshot» (создать снимок) (команда загрузки из изолированного хранилища в настольную систему) |
xd | Сокращение от XDE (эмулятор) |
{e0e7e3d7-c24b-498e-b88d-d7c2d4077a3b} | ProductID для вспомогательного приложения. Он указывается в WMAppManifest.xml и отличается для каждого приложения |
C:\Temp\IsoDump | Любой допустимый путь в настольной системе, куда вы хотите скопировать снимок |
После извлечения SDF-файла в настольную систему вспомогательное приложение больше не требуется и можно переключить свое внимание на приложение Mangolicious, использующее эту базу данных.
Использование базы данных
В приложении Mangolicious я добавляю SDF-файл в проект, а также включаю в решение тот же собственный класс DataContext, но с парой небольших изменений. В Mangolicious мне не требуется записб в базу данных, поэтому я могу использовать ее напрямую из папки установки приложения. Таким образом, строка подключения немного отличается от той, которая была во вспомогательном приложении. Кроме того, в коде Mangolicious определяется таблица SeasonalHighlights. Соответствующей таблицы в базе данных нет. Вместо этого ее код извлекает данные из двух таблиц нижележащей базы данных (Recipes и Cocktails) и использует их для заполнения элемента панорамы Seasonal Highlights. Вот и все, чем отличаются классы DataContext во вспомогательном и основном приложениях:
public class MangoDataContext : DataContext
{
public MangoDataContext()
: base("Data Source=appdata:/Mangolicious.sdf;File Mode=read only;") { }
public Table<Recipe> Recipes;
public Table<Fact> Facts;
public Table<Cocktail> Cocktails;
public Table<SeasonalHighlight> SeasonalHighlights;
}
В приложении Mangolicious также нужен класс ViewModel, и в качестве отправной точки можно задействовать класс ViewModel из вспомогательного приложения. Мне необходимы поле DataContext и группа свойств-наборов List<T> для табличных данных. И еще я добавлю строковое свойство, которое будет хранить название текущего сезона, вычисляемого в конструкторе:
public MainViewModel()
{
season = String.Empty;
int currentMonth = DateTime.Now.Month;
if (currentMonth >= 3 && currentMonth <= 5) season = "spring";
else if (currentMonth >= 6 && currentMonth <= 8) season = "summer";
else if (currentMonth >= 9 && currentMonth <= 11) season = "autumn";
else if (currentMonth == 12 || currentMonth == 1 || currentMonth == 2)
season = "winter";
}
В ViewModel критически важен метод LoadData. Здесь я инициализирую базу данных и выполняю запросы LINQ to SQL для загрузки данных через DataContext в свои наборы в памяти. Я мог бы в этот момент заранее загружать все три таблицы, но хочу оптимизировать время запуска приложения, отложив загрузку данных до открытия соответствующей страницы. Единственное, что я должен загрузить при запуске — это данные для таблицы SeasonalHighlight, потому что она показывается на основной странице. Для этого с помощью двух запросов я извлекаю записи из таблиц Recipes и Cocktails, соответствующие текущему сезону, и добавляю в набор скомбинированные группы записей, как показано на рис. 5.
Рис. 5. Загрузка данных при запуске
public void LoadData()
{
mangoDb = new MangoDataContext();
if (!mangoDb.DatabaseExists())
{
mangoDb.CreateDatabase();
}
var seasonalRecipes = from r in mangoDb.Recipes
where r.Season == season
select new { r.ID, r.Name, r.Photo };
var seasonalCocktails = from c in mangoDb.Cocktails
where c.Season == season
select new { c.ID, c.Name, c.Photo };
seasonalHighlights = new List<SeasonalHighlight>();
foreach (var v in seasonalRecipes)
{
seasonalHighlights.Add(new SeasonalHighlight {
ID = v.ID, Name = v.Name, Photo = v.Photo, SourceTable="Recipes" });
}
foreach (var v in seasonalCocktails)
{
seasonalHighlights.Add(new SeasonalHighlight {
ID = v.ID, Name = v.Name, Photo = v.Photo, SourceTable = "Cocktails" });
}
isDataLoaded = true;
}
Я могу использовать аналогичные запросы LINQ to SQL для создания отдельных методов LoadFacts, LoadRecipes и LoadCocktails, которые можно вызывать после запуска для загрузки соответствующих данных по требованию.
Создание UI
Основная страница состоит из Panorama с тремя PanoramaItem. Первый PanoramaItem содержит ListBox, предлагающий главное меню приложения. Когда пользователь выбирает один из элементов в ListBox, происходит переход к соответствующей странице, т. е. либо к страницам наборов для Recipes, Facts и Cocktails, либо к странице Game. Перед самым переходом я загружаю необходимые данные в наборы Recipes, Facts или Cocktails:
switch (CategoryList.SelectedIndex)
{
case 0:
App.ViewModel.LoadRecipes();
NavigationService.Navigate(
new Uri("/RecipesPage.xaml", UriKind.Relative));
break;
... additional cases omitted for brevity
}
Когда пользователь выбирает элемент из списка Seasonal Highlights в UI, я анализирую выбранный элемент, чтобы выяснить, что это — Recipe или Cocktail, а затем выполняю переход на индивидуальную страницу Recipe или Cocktail, передавая идентификатор этого элемента как часть строки запроса навигации (рис. 6).
Неявное применение стилей — еще одно новшество, появившееся в Windows Phone SDK 7.1 как часть Silverlight 4. |
Рис. 6. Выбор из списка Seasonal Highlights
SeasonalHighlight selectedItem =
(SeasonalHighlight)SeasonalList.SelectedItem;
String navigationString = String.Empty;
if (selectedItem.SourceTable == "Recipes")
{
App.ViewModel.LoadRecipes();
navigationString =
String.Format("/RecipePage.xaml?ID={0}", selectedItem.ID);
}
else if (selectedItem.SourceTable == "Cocktails")
{
App.ViewModel.LoadCocktails();
navigationString =
String.Format("/CocktailPage.xaml?ID={0}", selectedItem.ID);
}
NavigationService.Navigate(
new System.Uri(navigationString, UriKind.Relative));
Пользователь может перейти из меню на основной странице к одной из трех других страниц. В каждой из них данные связываются с одним из наборов в ViewModel для отображения списка элементов: Recipes, Facts или Cocktails. На каждой из этих страниц есть простой ListBox, где каждый элемент в списке содержит элемент управления Image для картинки и TextBlock для названия элемента. На рис. 7, к примеру, показана страница FactsPage.
Рис. 7. Интересные факты, одна из страниц списков-наборов
Когда пользователь выбирает индивидуальный элемент из списка Recipes, Facts или Cocktails, осуществляется переход к соответствующей странице (Recipe, Fact или Cocktail) и передается идентификатор этого элемента в строке запроса навигации. И вновь эти страницы почти идентичны, каждая из них предлагает Image и некоторый текст под ним. Заметьте, что я не определяю явный стиль для связанных с данными элементами TextBlock, но все они, тем не менее, используют TextWrapping=Wrap. Это делается объявлением стиля TextBlock в App.xaml.cs:
<Style TargetType="TextBlock" BasedOn="{StaticResource
PhoneTextNormalStyle}">
<Setter Property="TextWrapping" Value="Wrap"/>
</Style>
Результат заключается в том, что любой TextBlock в этом решении, для которого не указан собственный стиль явным образом, будет неявно использовать объявленный в App.xaml.cs. Неявное применение стилей — еще одно новшество, появившееся в Windows Phone SDK 7.1 как часть Silverlight 4.
Хотя механизм «возврата домой» можно реализовать, из-за его поведения следует хорошенько подумать, а стоит ли его вводить. |
Отделенный код для каждой из этих страниц весьма прост. В переопределенной версии OnNavigatedTo я извлекаю идентификатор элемента из строки запроса, нахожу этот элемент в наборе ViewModel и связываю его с данными. Код для RecipePage немного посложнее, чем для остальных: весь дополнительный код в этой странице относится к HyperlinkButton, размещаемой в правом верхнем углу страницы (рис. 8).
Рис. 8. Страница рецептов с кнопкой-заколкой
Функция Secondary Tiles
Когда пользователь щелкает «заколку» HyperlinkButton на странице индивидуального рецепта, я закрепляю этот элемент как тайл (картинку) на странице Start устройства Phone. Эта операция приводит к переходу пользователя на страницу Start и деактивирует приложения. Когда тайл закрепляется таким образом, он периодически анимируется, поворачиваясь то передней, то обратной стороной, как показано на рис. 9 и 10.
Рис. 9. Прикрепленный тайл рецепта (передняя сторона)
Рис. 10. Прикрепленный тайл рецепта (обратная сторона)
В последующем пользователь сможет стукнуть пальцем по этому закрепленному тайлу и перейти прямо к нужному элементу в приложении. После открытия соответствующей страницы на кнопке-заколке изображение сменится с утопленной заколки на поднятую заколку. Если он открепит страницу, она будет удалена со страницы Start и приложение продолжит свою работу.
Вот как это работает. В переопределенной версии OnNavigatedTo для RecipePage — после выполнения стандартный операций для определения того, какой Recipe следует связать через механизм привязки данных, — я формирую строку, которую впоследствии смогу использовать как URI для этой страницы:
thisPageUri = String.Format("/RecipePage.xaml?ID={0}", recipeID);
В обработчике щелчка кнопки-заколки я сначала проверяю, существует ли тайл для этой страницы, и, если нет, создаю его. Для этого используются данные текущего Recipe (рецепта): изображение и название. Я также задаю единственное статическое изображение и статический текст для обратной стороны тайла. В то же время я задействую способность кнопки перерисовывать себя, используя изображение открепленной заколки. С другой стороны, если тайл уже есть, я оказываюсь в обработчике щелчка, так как пользователь выбрал открепление тайла. В этом случае я удаляю тайл и перерисовываю кнопку, используя изображение заколки, как показано на рис. 11.
Рис. 11. Прикрепление и открепление страниц
private void PinUnpin_Click(object sender, RoutedEventArgs e)
{
tile = ShellTile.ActiveTiles.FirstOrDefault(
x => x.NavigationUri.ToString().Contains(thisPageUri));
if (tile == null)
{
StandardTileData tileData = new StandardTileData
{
BackgroundImage = new Uri(
thisRecipe.Photo, UriKind.RelativeOrAbsolute),
Title = thisRecipe.Name,
BackTitle = "Lovely Mangoes!",
BackBackgroundImage =
new Uri("Images/BackTile.png", UriKind.Relative)
};
ImageBrush brush = (ImageBrush)PinUnpin.Background;
brush.ImageSource =
new BitmapImage(new Uri("Images/Unpin.png", UriKind.Relative));
PinUnpin.Background = brush;
ShellTile.Create(
new Uri(thisPageUri, UriKind.Relative), tileData);
}
else
{
tile.Delete();
ImageBrush brush = (ImageBrush)PinUnpin.Background;
brush.ImageSource =
new BitmapImage(new Uri("Images/Pin.png", UriKind.Relative));
PinUnpin.Background = brush;
}
}
Заметьте: если пользователь касается пальцем закрепленного тайла, чтобы получить страницу этого рецепта, а затем нажимает аппаратную кнопку Back, то выходит из приложения. Потенциально это может запутывать, потому что пользователь ожидает выхода из приложения только при нажатии кнопки Back, находясь на основной странице, но не на любой другой. В качестве альтернативы можно было бы предоставить некую разновидность кнопки Home (домой) на странице Recipe и разрешить пользователю последовательно вернуться на основную страницу приложения. Увы, это тоже может ввести в замешательство, потому что пользователь, перейдя на основную страницу и нажав Back, он вернулся бы на прикрепленную страницу Recipe, а не вышел бы из приложения. Из-за такого поведения механизма «возврата домой» следует хорошенько подумать, а стоит ли его вводить.
Интеграция XNA-игры
Вспомните, что изначально я создал приложение как решение Windows Phone Silverlight and XNA Application. Тем самым я получил три проекта. Для создания функциональности, не имеющей отношения к игре, я работал над основным проектом MangoApp. Проект GameLibrary выступает в роли мостика между Silverlight MangoApp и XNA GameContent. На него ссылается как проект MangoApp, так и проект GameContent. Дополнительных усилий не требуется. Чтобы интегрировать игру в мое Phone-приложение, нужно решить две главные задачи:
- расширить класс GamePage в проекте MangoApp для включения всей игровой логики;
- асширить проект GameContent, чтобы он предоставлял изображения и звуки для игры (никаких изменений в коде не нужно).
Рассмотрим вкратце расширения, генерируемые Visual Studio для проекта, который интегрирует Silverlight и XNA. Первым делом обратите внимание на то, что в App.xaml объявляется SharedGraphicsDeviceManager. Он управляет совместным использованием экрана исполняющими средами Silverlight и XNA. Этот объект также является единственной причиной для наличия в проекте дополнительного класса AppServiceProvider. Данный класс используется для кеширования диспетчера устройства общей графики (shared graphics device manager), поэтому он доступен всем, кому это нужно в приложении, — как Silverlight, так и XNA. В классе App есть поле AppServiceProvider, и он также предоставляет некоторые дополнительные свойства для интеграции XNA: ContentManager и GameTimer. Все они инициализируются в новом методе InitializeXnaApplication, вместе с GameTimer, которое используется для «прокачки» очереди сообщений XNA.
Интересная работа заключается в том, как интегрировать XNA-игру в Phone-приложение Silverlight. Интересная работа заключается в том, как интегрировать XNA-игру в Phone-приложение Silverlight. Сама по себе игра представляет меньший интерес. Поэтому в данном упражнении, вместо того чтобы тратить силы на написание полноценной игры с нуля, я адаптирую существующую, а именно: пример из учебного пособия по разработке игр на платформе XNA с сайта AppHub (bit.ly/h0ZM4o). В моей адаптации имеется шейкер (для приготовления коктейлей), представленный в коде классом Player; он стреляет метательным предметом в приближающиеся плоды манго (это враги). Когда я попадаю в манго, он раскрывается и преобразуется в коктейль mangotini. Каждое попадание в манго увеличивает счет на 100 очков. Всякий раз, когда манго сталкивается с шейкером, его сила уменьшается на 10. Когда сила падает до 0, игра заканчивается. Кроме того, пользователь может закончить игру в любой момент, нажав кнопку аппаратную Back. Игра в действии показана на рис. 12. |
Рис. 12. XNA-игра в действии
Мне не нужно вносить какие-либо изменения в почти пустой GamePage.xaml. Вместо этого вся работа пойдет в отделенном коде. Visual Studio генерирует стартовый код для этого класса GamePage, как описано в табл. 3.
Табл. 3. Стартовый код для GamePage
Поле/метод | Описание | Необходимые изменения |
ContentManager | Загружает/управляет временем жизни контента из конвейера контента (content pipeline) | Добавить код, чтобы использовать этот метод для загрузки изображений и звуков |
GameTimer | В модели XNA игра выполняет действия, когда срабатывают события Update и Draw, и эти события управляются таймером | Без изменений |
SpriteBatch | Используется для прорисовки текстур в XNA | Добавить код, чтобы использовать этот метод в методе Draw для прорисовки игровых объектов (игрока, врагов, летящих предметов, взрывов и т. д.) |
GamePage Constructor | Создает таймер и подключает его события Update и Draw к методам OnUpdate и OnDraw | Сохранить код таймера и дополнительно инициализировать игровые объекты |
OnNavigatedTo | Настраивает совместное использование графики между Silverlight и XNA, а потом запускает таймер | Сохранить код совместного использования и таймера; дополнительно загружать контент в игру, в том числе любое предыдущее состояние из изолированного хранилища |
OnNavigatedFrom | Останавливает таймер и отключает совместное использование графики XNA | Сохранить код совместного использования и таймера; дополнительно сохранять игровой счет и здоровье игрока в изолированном хранилище |
OnUpdate | (Пустой), обрабатывает событие GameTimer.Update | Добавить код для расчета изменений игровых объектов (позиции игрока, количества и позиций врагов, летящих предметов и взрывов) |
OnDraw | (Пустой), обрабатывает событие GameTimer.Draw | Добавить код для прорисовки игровых объектов, вывода счета и здоровья игрока |
Эта игра является прямой адаптацией примера из учебного пособия AppHub, в котором содержатся два проекта: проект игры Shooter и проект контента ShooterContent. Контент включает файлы изображений и звуков. Хотя это не влияет на код приложения, я могу изменить эти файлы для подстройки под тематику «манго» своего приложения, и для этого достаточно заменить PNG- и WAV-файлы. Все необходимые изменения кода вносятся в проект игры Shooter. Руководство по переносу из Game Class в Silverlight/XNA вы найдете на сайте AppHub по ссылке bit.ly/iHl3jz.
Для начала я должен скопировать файлы проекта игры Shooter в свой проект MangoApp. Кроме того, я копирую файлы контента ShooterContent в проект GameContent. В табл. 4 дано сводное описание существующих классов в проекте игры Shooter.
Табл. 4. Классы игры Shooter
Класс | Описание | Необходимые изменения |
Animation | Анимирует различные спрайты в игре: игрока, вражеские объекты, летящие предметы и взрывы | Исключить GameTime |
Enemy | Спрайт, представляющий вражеские объекты, по которым стреляет игрок. В моей адаптации это плоды манго | Исключить GameTime |
Game1 | Управляющий класс для игры | Объединить с классом GamePage |
ParallaxingBackground | Анимирует фоновые изображения облаков, создавая за счет параллакса кажущуюся объемность | Нет |
Player | Спрайт, представляющий игровой персонаж. В моей адаптации это шейкер для коктейлей | Исключить GameTime |
Program | Применяется только для игр, ориентированных на Windows или Xbox | Не используется; можно удалить |
Projectile | Спрайт, представляющий летящие предметы, которыми игрок стреляет по врагам | Нет |
Чтобы интегрировать эту игру в мое Phone-приложение, в класс GamePage нужно внести следующие изменения.
- Скопировать все поля из класса Game1 в класс GamePage. Также скопировать поле инициализации в методе Game1.Initialize в конструктор GamePage.
- Скопировать метод LoadContent и все методы для добавления и обновления врагов, летящих предметов и взрывов. Никаких изменений в этих методах не требуется.
- Перевести код на использование вместо GraphicsDeviceManager свойства GraphicsDevice.
- Извлечь код в методах Game1.Update и Draw в обработчики событий таймера GamePage.OnUpdate и OnDraw.
Стандартная XNA-игра создает новый GraphicsDeviceManager, тогда как в Phone-приложении у меня уже есть SharedGraphicsDeviceManager, который предоставляет свойство GraphicsDevice, и этого более чем достаточно. Чтобы упростить картину, я буду кешировать ссылку на GraphicsDevice как поле в своем классе GamePage.
В стандартной XNA-игре методы Update и Draw являются переопределенными версиями виртуальных методов базового класса Microsoft.Xna.Framework.Game. Однако в интегрированном приложении Silverlight/XNA класс GamePage не наследует от XNA-класса Game, поэтому я должен абстрагировать код от методов Update и Draw и вставить их содержимое в обработчики событий OnUpdate и OnDraw. Заметьте, что некоторые классы игровых объектов (например, Animation, Enemy и Player), методы Update и Draw, а также ряд вспомогательных методов, вызываемых Update, принимают параметр GameTime. Он определен в Microsoft.Xna.Framework.Game.dll, и, если приложение Silverlight содержит любые ссылки на эту сборку, это, как правило, следует считать ошибкой. Параметр GameTime можно полностью заменить двумя свойствами Timespan — TotalTime и ElapsedTime, — предоставляемыми объектом GameTimerEventArgs, который передается в обработчики событий таймера OnUpdate и OnDraw. Ну а в остальном содержимое метода Draw можно перенести без изменений.
Исходный метод Update проверяет состояние GamePad и вызывает Game.Exit по условию. В интегрированном приложении Silverlight/XNA это не используется, поэтому переносить такую проверку в новый метод незачем:
//if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed)
//{
// // this.Exit();
//}
Новый метод Update теперь не просто обвязка, откуда вызываются другие методы для обновления игровых объектов. Я обновляю фон с эффектом параллакса даже по окончании игры, но игровой персонаж, враги, соударения, летящие предметы и взрывы обновляются, только если игровой персонаж жив. Соответствующие вспомогательные методы вычисляют количество и позиции различных игровых объектов. После исключения использования GameTime всех их можно перенести без изменений за одним исключением:
private void OnUpdate(object sender, GameTimerEventArgs e)
{
backgroundLayer1.Update();
backgroundLayer2.Update();
if (isPlayerAlive)
{
UpdatePlayer(e.TotalTime, e.ElapsedTime);
UpdateEnemies(e.TotalTime, e.ElapsedTime);
UpdateCollision();
UpdateProjectiles();
UpdateExplosions(e.TotalTime, e.ElapsedTime);
}
}
Метод UpdatePlayer нужно в небольшой оптимизации. В исходной версии игры, когда здоровье игрока падало до 0, оно восстанавливалось до 100, т. е. игра продолжалась вечно. В моей адаптации, когда здоровье игрока падает до 0, я устанавливаю флаг в false. Затем в методах OnUpdate и OnDraw проверяю значение этого флага. В OnUpdate значение флага определяет, надо ли далее вычислять изменения в объектах, а в OnDraw он указывает, что именно следует рисовать — игровые объекты или экран «game over» с финальным счетом:
private void UpdatePlayer(TimeSpan totalTime, TimeSpan elapsedTime)
{
...unchanged code omitted for brevity.
if (player.Health <= 0)
{
//player.Health = 100;
//score = 0;
gameOverSound.Play();
isPlayerAlive = false;
}
}
Заключение
В этой статье мы рассмотрели, как разрабатывать приложения с применением нескольких новых средств в Windows Phone SDK 7.1: локальных баз данных, LINQ to SQL, вторичных тайлов и глубокого связывания, а также интеграцию Silverlight и XNA. В выпуске SDK 7.1 содержится гораздо больше новых средств и усовершенствований в существующих. Подробнее на эту тему см. следующие ссылки.
Финальная версия приложения Mangolicious доступна в Windows Phone Marketplace по ссылке bit.ly/nuJcTA (для доступа потребуется программное обеспечение Zune). Обратите внимание, что пример использует Silverlight for Windows Phone Toolkit (можно бесплатно скачать по ссылке bit.ly/qiHnTT).