Windows Phone SDK 7.1

构建“Mango”应用程序

Andrew Whitechapel

下载代码示例

“Mango”(芒果)是 Windows Phone SDK 7.1 版本的内部代码名称,当然也是一种美味的热带水果的名字。 芒果的用途多种多样 — 例如,用在馅饼、色拉和各种鸡尾酒中。 据说芒果对于人体健康还有诸多好处,还有十分有趣的文化史。 在本文中,我将讨论 Mangolicious,这是一个关于芒果的 Windows Phone SDK 7.1 应用程序。 该应用程序提供各种芒果食谱、鸡尾酒和小知识,不过我们真正的目的是探讨 7.1 版本中的主要新增功能,具体而言就是:

  • 本地数据库和 LINQ to SQL
  • 辅助图标和深层链接
  • Silverlight/XNA 集成

该应用程序的用户体验十分简单:主页提供了一个全景,全景第一项是一个菜单,第二项是应季食谱和鸡尾酒的动态精选,第三项是一些简单的“关于”信息,如图 1 所示。

Mangolicious Panorama Main Page
图 1 Mangolicious 全景主页

应季集锦部分中的菜单和项都是通往应用程序其他页的链接。 大多数页都是简单明了的 Silverlight 页,其中一页是 XNA 集成游戏专用的页。 构建这个应用程序从头到尾需要完成的任务,总结如下:

  1. 在 Visual Studio 中创建基本解决方案。
  2. 为食谱、鸡尾酒和小知识数据独立创建数据库。
  3. 更新应用程序,以使用数据库并公开数据库以便进行数据绑定。
  4. 创建各 UI 页,对这些 UI 页进行数据绑定。
  5. 设置辅助图标功能,使用户可以将食谱项固定到手机的起始页。
  6. 在应用程序中集成 XNA 游戏。

创建解决方案

对于这个应用程序,我将在 Visual Studio 中使用 Windows Phone Silverlight and XNA Application 模板。 这样会生成一个包含三个项目的解决方案;重新命名之后,这些项目汇总如图 2 所示。

图 2:Windows Phone Silverlight and XNA 解决方案中的项目

项目 说明
MangoApp 包含手机应用程序本身,以及默认主页和辅助游戏页。
GameLibrary 基本上是个空项目,包含所有正确引用,但没有代码。 重要的是,它包含对 Content 项目的内容引用。
GameContent 一个空的 Content 项目,包含所有游戏资产(图像、声音文件等等)。

创建数据库和 DataContext 类

Windows Phone SDK 7.1 版本引入了对本地数据库的支持。 也就是说,应用程序可以将数据存储在手机的本地数据库文件 (SDF) 中。 建议在代码中创建数据库,要么作为应用程序本身的组成部分创建,要么通过单纯为创建数据库而构建的独立帮助应用程序创建。 如果只在应用程序运行时才创建大多数或所有数据,则最好在应用程序内创建数据库。 至于 Mangolicious 应用程序,我只有静态数据,可以预先填充数据库。

为此,我将单独创建用于创建数据库的帮助应用程序,首先从简单的 Windows Phone Application 模板开始。 要在代码中创建数据库,我需要一个从 DataContext 派生的类(在 System.Data.Linq 程序集的自定义 Phone 版本中定义)。 这个 DataContext 类既可以在创建数据库的帮助应用程序中使用,也可以在使用数据库的主应用程序中使用。 在该帮助应用程序中,必须将数据库位置指定在独立存储中,因为这是唯一可以从手机应用程序写入数据的位置。 该类还包含每个数据库表的一组表字段:

public class MangoDataContext : DataContext
{
  public MangoDataContext()
    : base("Data Source=isostore:/Mangolicious.sdf") { }
 
  public Table<Recipe> Recipes;
  public Table<Fact> Facts;
  public Table<Cocktail> Cocktails;
}

代码中的表类与数据库中的表之间存在 1:1 的映射关系。 列属性映射到数据库表的列,包括数据类型和大小(INT、NVARCHAR 等等)、列是否可为 null、是否键列等数据库架构属性。 我以相同的方式为数据库中的所有其他表定义表类,如图 3 所示。

图 3:定义表类

[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;
      }
    }
  }?
...
additional column definitions omitted for brevity
}

另外,在帮助应用程序中 — 使用标准 Model-View-ViewModel (MVVM) 方法 — 我现在需要一个 ViewModel 类,以便使用 DataContext 类连接视图 (UI) 与模型(数据)。 ViewModel 有一个 DataContext 字段以及一组表数据集合(Recipes、Facts 和 Cocktails)。 这些是静态数据,因此在这里使用 List<T> 集合就足够了。 同样的原因,我只需要 get 属性访问器,不需要 set 修饰符(请参见图 4)。

图 4:为 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 表采用同样的模式(请参见图 5)。

图 5:创建数据库

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 方法。 运行帮助应用程序(在仿真器中或在物理设备上)时,会在应用程序的独立存储创建数据库文件。 最后的任务是将该文件提取到桌面,以便在主应用程序中使用。 为此,我将使用 Windows Phone SDK 7.1 附带的命令行工具:Isolated Storage Explorer 工具。 下面是从仿真器获取独立存储快照并放置于桌面的命令:

"C:\Program Files\Microsoft SDKs\Windows Phone\v7.1\Tools\IsolatedStorageExplorerTool\ISETool" ts xd {e0e7e3d7-c24b-498e-b88d-d7c2d4077a3b} C:\Temp\IsoDump

此命令假定该工具安装在标准位置。 参数说明请参见图 6

图 6:Isolated Storage Explorer 命令行参数

参数 说明
ts “获取快照”(用于从独立存储下载至桌面的命令)。
xd XDE(即仿真器)的缩写。
{e0e7e3d7-c24b-498e-b88d-d7c2d4077a3b} 帮助应用程序的 ProductID。 此项列在 WMAppManifest.xml 中,并且对于每个应用程序均不相同。
C:\Temp\IsoDump 要将快照复制到的任何有效桌面路径。

将 SDF 文件提取到桌面后,就不需要使用帮助应用程序了,我可以将精力转到将使用此数据库的 Mangolicious 应用程序。

使用数据库

在 Mangolicious 应用程序中,将 SDF 文件添加到项目中,将上面的自定义 DataContext 类添加到解决方案中,并进行稍许更改。 在 Mangolicious 中,我不需要写入数据库,可从应用程序安装文件夹直接使用数据库。 因此,连接字符串略微不同于帮助应用程序中的连接字符串。 此外,Mangolicious 在代码中定义 SeasonalHighlights 表。 数据库中没有对应的 SeasonalHighlight 表。 此代码表从两个基础数据库表(Recipes 和 Cocktails)中取出数据,用于填充应季集锦全景项。 这两处更改是数据库创建帮助应用程序与 Mangolicious 数据库使用应用程序中的 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 表中符合当前季节的行,并将组合行集添加到集合,如图 7 所示。

图 7:启动时加载数据

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

主页包含一个全景,其中有三个全景项。 第一项是一个列表框,用于提供应用程序主菜单。 如果用户选择其中一个列表框项,则会导航至相应页(即 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
}

如果用户在 UI 中选择应季集锦列表中的某一项,则检查所选项是菜谱还是鸡尾酒,然后导航至菜谱或鸡尾酒页,传入项 ID 作为导航查询字符串的一部分,如图 8 所示。

图 8:在应季集锦列表中选择

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。 这些页都提供一个简单的列表框,列表框中的每一项都包含一个 Image 控件(显示照片)和一个 TextBlock(显示项名称)。 例如,图 9 显示的是 FactsPage。

Fun Facts, One of the Collection List Pages
图 9:Fun Facts,集合列表页之一

如果用户选择 Recipes、Facts 或 Cocktails 列表中的各项,则会导航至相应的菜谱、小知识或鸡尾酒页,并在导航查询字符串中传递相应的项 ID。 同样,这三个类型的这些页几乎都相同,每页都提供一个图像,并在图像下配以文字。 请注意,我不显式定义数据绑定 TextBlock 的样式,但它们都会使用 TextWrapping=Wrap。 这是通过在 App.xaml.cs 中声明 TextBlock 样式完成的:

<Style TargetType="TextBlock" BasedOn="{StaticResource
  PhoneTextNormalStyle}">
  <Setter Property="TextWrapping" Value="Wrap"/>
</Style>

其结果是,解决方案中任何未明确定义自身样式的 TextBlock 都隐式采用这一样式。 隐式样式是 Windows Phone SDK 7.1 中 Silverlight 4 的另一新增功能。

所有这些页的代码隐藏都很简单。 在 OnNavigatedTo 重写中,我从查询字符串中提取各个项 ID,在 ViewModel 集合中找到相应项,然后进行数据绑定。 RecipePage 的代码略复杂于其他代码 — 此页中的附加代码都与页右上角的 HyperlinkButton 有关。 请参见图 10

A Recipe Page with Pin Button
图 10:带图钉按钮的菜谱页

辅助图标

如果用户单击各菜谱页中的“图钉”超链接按钮,会将相应项固定为手机起始页上的辅助图标。 固定操作将用户导航至起始页并停用该应用程序。 如果以这种方式固定辅助图标,该图标会周期性展示动画效果,不断翻转显示正面和背面,如图 11图 12 所示。

Pinned Recipe Tile (Front)
图 11:固定的菜谱辅助图标(正面)

Pinned Recipe Tile (Back)
图 12:固定的菜谱辅助图标(背面)

之后,如果用户单击这个固定辅助图标,则会直接导航至应用程序内的相应项。 到达该页时,“图钉”按钮将显示“解除固定”图像。 如果用户解除固定该页,该页则会从起始页中移除,应用程序继续运行。

下面是工作原理。 在 RecipePage 的 OnNavigatedTo 重写中,在完成用于确定对哪个特定 Recipe 进行数据绑定的标准操作之后,我构造一个稍后可用作此页 URI 的字符串:

thisPageUri = String.Format("/RecipePage.xaml?ID={0}", recipeID);

在“图钉”按钮的按钮单击处理程序中,首先检查此页是否已有辅助图标,如果没有,则立即创建。 我使用当前 Recipe 数据创建该辅助图标:图像和名称。 我还设置一张静态图像(以及静态文字),供辅助图标背面使用。 同时,我利用机会使用“解除固定”图像重新绘制按钮自身。 另一方面,如果辅助图标存在,因为用户已选择解除固定辅助图标,则必须在单击处理程序中, 在这里,删除辅助图标并使用“图钉”图像重新绘制按钮,如图 13 所示。

图 13:固定和解除固定页

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;
  }
}

请注意,如果用户单击固定辅助图标转至菜谱页,然后按手机的硬件后退键,则会完全退出应用程序。 这可能造成混淆,因为用户通常认为只有在主页按后退才会退出应用程序,在任何其他页按后退不会退出应用程序。 不过,另一种方式可以在菜谱页中提供某种“归位”按钮,允许用户退回应用程序的其余部分。 很遗憾,这也会造成混淆,因为当用户到达主页并按后退键时,用户会退回固定的菜谱页,而不是退出应用程序。 因此,尽管“归位”机制并非无法实现,在引入前应仔细权衡。

集成 XNA 游戏

最初,我创建这个应用程序是想实现一个 Windows Phone Silverlight and XNA Application 解决方案。 这样就有了三个项目。 我将主要的 MangoApp 构建成非游戏功能项目。 GameLibrary 项目充当 Silverlight MangoApp 与 XNA GameContent 之间的“桥梁”。 MangoApp 项目引用该项目,该项目自己则引用 GameContent 项目。 这需要进一步的工作。 要将游戏集成到手机应用程序中,需要完成两项主要任务:

  • 增强 MangoApp 项目中的 GamePage 类,使它包含所有游戏逻辑。
  • 增强 GameContent 项目以提供游戏的图像和声音(无代码更改)。

大致了解一下 Visual Studio 为集成 Silverlight 和 XNA 的项目所生成的增强功能,首先要注意的,是 App.xaml 声明了一个 SharedGraphicsDeviceManager。 它管理 Silverlight 与 XNA 运行时之间的屏幕共享。 这一目的也是项目中附加 AppServiceProvider 类的唯一原因。 该类用于缓存共享图形设备管理器, 以便供应用程序(Silverlight 和 XNA)在需要时使用。 App 类有一个 AppServiceProvider 字段,还为进行 XNA 集成公开了一些附加属性:ContentManager 和 GameTimer。 与 GameTimer(用于抽取 XNA 消息队列)一道,这些属性都在新的 InitializeXnaApplication 方法中初始化。

有趣的工作是如何在 Silverlight 手机应用程序中集成 XNA 游戏。 游戏自身实际上还没那么有趣。 因此,在本练习中,我不是从头开始编写完整的游戏,而是改写一个现有游戏,具体说就是 AppHub 上的 XNA 游戏教程:bit.ly/h0ZM4o

在我的改写内容中,有一个由代码中的 Player 类表示的鸡尾酒调酒器,它瞄准来袭的芒果(敌人)发射炮弹。 如果击中芒果,芒果就裂开变成果桨。 每次击中芒果都能得到 100 分。 鸡尾酒调酒器每次与芒果相碰,玩家场强都会消弱 10。 当场强变为零,游戏结束。 用户也可以随时按手机的后退键正常终止游戏。 图 14 显示真正运行的游戏。

The XNA Game in Progress
图 14:正在运行的 XNA 游戏

我不需要对(几乎是空的)GamePage.xaml 进行任何更改。 所有工作都在代码隐藏中完成。 Visual Studio 为 GamePage 类生成起始代码,如图 15 所示。

图 15:GamePage 起始代码

字段/方法 用途 需要的更改
ContentManager 加载和管理内容管道的内容的生存期。 添加代码,以便用于加载 图像和声音。
GameTimer 在 XNA 游戏模型中,如果触发 Update 和 Draw 事件,游戏就执行操作,这些事件是由计时器控制的。 无更改。
SpriteBatch 用于在 XNA 中绘制纹理。 添加代码,以便在 Draw 方法中用于绘制游戏对象(玩家,敌人,炮弹,爆炸等等)。
GamePage 构造函数 创建计时器,并将其 Update 和 Draw 事件挂钩到 OnUpdate 和 OnDraw 方法。 保留计时器代码,另外初始化游戏对象。
OnNavigatedTo 在 Silverlight 与 XNA 之间设置图形共享,并启动计时器。 保留共享和计时器代码,另外将内容加载到游戏中,包括独立存储的 所有以往状态。
OnNavigatedFrom 停止计时器并关闭 XNA 图形共享。 保留计时器和共享代码,另外将游戏得分和玩家状况存储到独立存储。
OnUpdate (空),处理 GameTimer.Update 事件。 添加代码,以计算游戏对象更改 (玩家位置,敌人的数目和位置,炮弹和爆炸)。
OnDraw (空),处理 GameTimer.Draw 事件。 添加代码,以绘制游戏对象、游戏得分和玩家状况。

本游戏是对 AppHub 教程的直接改写,该教程包含两个项目:Shooter 游戏项目和 ShooterContent 内容项目。 内容包含图像文件和声音文件。 尽管不会影响应用程序代码,但我可以更改这些文件以符合应用程序的芒果主题,而这只需要替换 PNG 文件和 WAV 文件。 需要的(代码)更改都在 Shooter 游戏项目中。 关于从 Game 类迁移到 Silverlight/XNA 的指导原则请参见 AppHub:bit.ly/iHl3jz

首先,必须将 Shooter 游戏项目文件复制到现有的 MangoApp 项目中。 另外,将 ShooterContent 内容文件复制到现有的 GameContent 项目中。 图 16 汇总了 Shooter 游戏项目中的现有类。

图 16:Shooter 游戏类

用途 需要的更改
Animation 实现游戏中各种精灵的动画效果:玩家、敌人对象、炮弹和爆炸。 不使用 GameTime。
Enemy 表示用户炮击的 敌方对象的精灵。 在我改写的项目中,就是芒果。 不使用 GameTime。
Game1 游戏的控制类。 合并到 GamePage 类中。
ParallaxingBackground 实现云朵背景图像的动画效果,以呈现 3D 视差效应。 无。
Player 表示游戏中用户角色的 精灵。 在我改写的项目中,就是鸡尾酒调酒器。 不使用 GameTime。
Program 仅当游戏面向 Windows 或 Xbox 时才使用。 未使用;可以删除。
Projectile 表示玩家向敌人发射的炮弹的精灵。 无。

为了将这款游戏集成到手机应用程序中,我需要对 GamePage 类进行以下更改:

  • 从 Game1 类将所有字段复制到 GamePage 类中。 另外将 Game1.Initialize 方法中的字段初始化复制到 GamePage 构造函数中。
  • 复制 LoadContent 方法,以及用于添加和更新敌人、炮弹和爆炸的所有方法。 这些都不需要任何更改。
  • 停止使用 GraphicsDeviceManager,转而使用 GraphicsDevice 属性。
  • 将 Game1.Update、Draw 方法中的代码提取到 GamePage.OnUpdate 和 OnDraw 计时器事件处理程序中。

传统的 XNA 游戏会创建新的 GraphicsDeviceManager,而在手机应用程序中已有公开 GraphicsDevice 属性的 SharedGraphicsDeviceManager,这些对我来说已经足够。 为了简单起见,我将对 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 参数可由传入 OnUpdate 和 OnDraw 计时器事件处理程序的 GameTimerEventArgs 对象所公开的两个 Timespan 属性(TotalTime 和 ElapsedTime)完全替代。 除 GameTime 外,我还可以原样移植 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 中,该值确定是绘制对象还是绘制游戏结束屏幕及最终得分:

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 集成。 7.1 版本还提供很多其他新增功能,以及对现有功能的增强。 有关进一步详细信息,请参见以下链接:

Windows Phone Marketplace bit.ly/nuJcTA 提供 Mangolicious 应用程序的最终版本(请注意:需要 Zune 软件才能访问)。 请注意,示例使用 Silverlight for Windows Phone Toolkit(bit.ly/qiHnTT 提供免费下载)。

Andrew Whitechapel * 拥有 20 多年开发工作经验,目前担任 Windows Phone 团队的项目经理,负责应用程序平台的核心部分。*

衷心感谢以下技术专家对本文的审阅:Nick GravelynBrian HudsonHimadri Sarkar