Windows Phone SDK 7.1

Criando um aplicativo para o 'Mango'

Andrew Whitechapel

Baixar o código de exemplo

“Mango” é o codinome interno do Windows Phone SDK versão 7.1 e, naturalmente, é o nome de uma deliciosa fruta tropical. As mangas podem ser consumidas de várias maneiras: em tortas, saladas e em vários coquetéis. A manga também é conhecida pelos seus benefícios à saúde e possui um interessante histórico cultural. Neste artigo, vou examinar o Mangolicious, um aplicativo do Windows Phone SDK 7.1 sobre mangas. O aplicativo fornece várias receitas, coquetéis e fatos sobre a manga, mas a finalidade real é explorar alguns dos grandes recursos novos da versão 7.1, especificamente:

  • Banco de dados local e LINQ to SQL
  • Mosaicos secundários e vinculação profunda
  • Integração com Silverlight/XNA

A experiência do usuário do aplicativo é simples: a página principal oferece um panorama, com um menu no primeiro item de panorama, uma seleção dinâmica de receitas e coquetéis da estação no segundo item, e algumas simples informações “sobre” no terceiro item, como mostra a Figura 1.

Mangolicious Panorama Main Page
Figura 1 Página principal de panorama do Mangolicious

O menu e os itens da seção Seasonal Highlights funcionam como links para navegar até as outras páginas do aplicativo. A maioria das páginas é simplesmente do Silverlight, e uma delas é dedicada a um jogo XNA integrado. Aqui está um resumo das tarefas necessárias para criar esse aplicativo, do início ao fim:

  1. Crie a solução básica no Visual Studio.
  2. Independentemente, crie o banco de dados para os dados de receita, coquetel e fatos.
  3. Atualize o aplicativo para consumir o banco de dados e exponha-o para vinculação de dados.
  4. Crie as várias páginas de interface do usuário e vincule-as aos dados.
  5. Configure o recurso de mosaicos secundários para permitir que o usuário fixe itens de receita na home page do telefone.
  6. Incorpore um jogo XNA no aplicativo.

Criar a solução

Para este aplicativo, usarei o modelo Windows Phone Silverlight and XNA Application no Visual Studio. Ele gera uma solução com três projetos; após a renomeação, eles são resumidos na Figura 2.

Figura 2 Projetos em uma solução Windows Phone Silverlight and XNA

Projeto Descrição
MangoApp Contém o próprio aplicativo para telefone, com uma MainPage padrão e uma GamePage secundária.
GameLibrary Um projeto essencialmente vazio que possui todas as referências corretas, mas nenhum código. Crucialmente, ele inclui uma referência ao conteúdo para o projeto de conteúdo.
GameContent Um projeto de conteúdo vazio, que irá conter todos os ativos de jogos (imagens, arquivos de som, etc.).

Criar o banco de dados e a classe DataContext

O Windows Phone SDK versão 7.1 introduz o suporte para bancos de dados locais. Ou seja, o aplicativo pode armazenar dados em um arquivo de banco de dados local (SDF) no telefone. A abordagem recomendada é criar o banco de dados em código, como parte do próprio aplicativo ou através de um aplicativo auxiliar separado, que você desenvolve apenas para criar o banco de dados. Faz sentido criar o banco de dados dentro do aplicativo, em cenários nos quais você estará criando a maioria ou todos os dados apenas quando o aplicativo for executado. Para o aplicativo Mangolicious, tenho somente dados estáticos e posso popular o banco de dados com antecedência.

Para isso, vou criar um aplicativo auxiliar separado, criador de banco de dados, começando com o modelo simples do aplicativo para Windows Phone. Para criar o banco de dados em código, preciso de uma classe derivada do DataContext, que é definida na versão personalizada do Phone do assembly System.Data.Linq. Essa mesma classe DataContext pode ser usada no aplicativo auxiliar que cria o banco de dados e no aplicativo principal que consome o banco de dados. No aplicativo auxiliar, preciso especificar que o local do banco de dados seja em armazenamento isolado, pois esse é o único local onde é possível gravar a partir de um aplicativo para telefone. A classe também contém um conjunto de campos Table para cada tabela do banco de dados:

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

Existe um mapeamento 1:1 entre as classes Table no código e as tabelas no banco de dados. As propriedades Column mapeiam para as colunas na tabela do banco de dados e incluem as propriedades de esquema do banco de dados, como o tipo de dados e o tamanho (INT, NVARCHAR, etc.), se a coluna pode ser nula, se é uma coluna de chave, etc. Eu defino as classes de todas as outras tabelas no banco de dados da mesma maneira mostrada na Figura 3.

Figura 3 Definindo as classes 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;
      }
    }
  }?
    ... additional column definitions omitted for brevity
}

Ainda, no aplicativo auxiliar, e usando uma abordagem padrão MVVM (Model-View-ViewModel), eu agora preciso de uma classe ViewModel para mediar entre a View (a interface do usuário) e o Model (os dados) usando a classe DataContext. O ViewModel possui um campo DataContext e um conjunto de coleções para os dados da tabela (Recipes, Facts e Cocktails). Os dados são estáticos, portanto, coleções List<T> são suficientes aqui. Pelo mesmo motivo, eu só preciso de acessadores de propriedade get, não modificadores set (veja a Figura 4).

Figura 4 Definindo propriedades de coleção de dados da tabela no 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
}

Eu também exponho um método público (que geralmente invoco na interface do usuário), para realmente criar o banco de dados e todos os dados. Nesse método, eu crio o próprio banco de dados, caso ele ainda não exista, e crio cada tabela, populando cada uma com os dados estáticos. Por exemplo, para criar a tabela Recipe, eu crio várias instâncias da classe Recipe, correspondentes às linhas da tabela; adiciono todas as linhas da coleção ao DataContext; e, finalmente, confirmo os dados no banco de dados. O mesmo padrão é usado nas tabelas Facts e Cocktails (veja a Figura 5).

Figura 5 Criando o banco de dados

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

Em um ponto adequado do aplicativo auxiliar (talvez em um manipulador de clique de botão), posso então invocar esse método CreateDatabase. Quando eu executar o auxiliar (seja no emulador ou em um dispositivo físico), o arquivo de banco de dados será criado no armazenamento isolado do aplicativo. A tarefa final é extrair esse arquivo na área de trabalho para poder usá-lo no aplicativo principal. Para isso, eu usarei a ferramenta Isolated Storage Explorer, uma ferramenta da linha de comando fornecida com o Windows Phone SDK 7.1. Aqui está o comando para obter um instantâneo do armazenamento isolado do emulador para a área de trabalho:

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

Esse comando pressupõe que a ferramenta esteja instalada em um local padrão. Os parâmetros são explicados na Figura 6.

Figura 6 Parâmetros da linha de comando da ferramenta Isolated Storage Explorer

Parâmetro Descrição
ts “Tirar instantâneo” (o comando para baixar do armazenamento isolado para a área de trabalho).
xd Abreviação de XDE (isto é, o emulador).
{e0e7e3d7-c24b-498e-b88d-d7c2d4077a3b} A ProductID do aplicativo auxiliar. Ela é listada no arquivo WMAppManifest.xml e é diferente para cada aplicativo.
C:\Temp\IsoDump Qualquer caminho válido na área de trabalho em que você deseja copiar o instantâneo.

Após extrair o arquivo SDF na área de trabalho, eu concluí o uso do aplicativo auxiliar e posso voltar minha atenção para o aplicativo Mangolicious que consumirá esse banco de dados.

Consumir o banco de dados

No aplicativo Mangolicious, eu adiciono o arquivo SDF ao projeto e também adiciono a mesma classe DataContext personalizada à solução, com algumas alterações secundárias. No Mangolicious, não é necessário gravar no banco de dados, então eu posso usá-lo diretamente da pasta de instalação do aplicativo. Portanto, a cadeia de conexão é ligeiramente diferente da cadeia do aplicativo auxiliar. Além disso, o Mangolicious define uma tabela SeasonalHighlights em código. Não existe nenhuma tabela SeasonalHighlight correspondente no banco de dados. Em vez disso, essa tabela de códigos puxa os dados de duas tabelas de banco de dados subjacentes (Recipes e Cocktails) e é usada para popular o item de panorama Seasonal Highlights. Essas duas alterações são as únicas diferenças na classe DataContext entre o aplicativo auxiliar de criação do banco de dados e o aplicativo que consome o banco de dados, Mangolicious:

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

O aplicativo Mangolicious também precisa de uma classe ViewModel, e eu posso usar a classe ViewModel no aplicativo auxiliar como um ponto de partida. Preciso do campo DataContext e do conjunto de propriedades da coleção List<T> para as tabelas de dados. Acima de tudo, vou adicionar uma propriedade de cadeia de caracteres para registrar a estação atual, computada no construtor:

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

O método crítico no ViewModel é o método LoadData. Aqui, eu inicializo o banco de dados e executo consultas LINQ-to-SQL para carregar os dados por meio de DataContext nas minhas coleções na memória. Eu poderia pré-carregar todas as três tabelas neste ponto, mas desejo otimizar o desempenho de inicialização atrasando o carregamento dos dados, a não ser que (e até que) a página relevante seja realmente visitada. Os únicos dados que eu devo carregar na inicialização são os da tabela SeasonalHighlight, pois eles são exibidos na página principal. Para isso, tenho duas consultas para selecionar apenas as linhas das tabelas Recipes e Cocktails que correspondam à estação atual, e adicionar à coleção os conjuntos de linhas combinados, como mostra a Figura 7.

Figura 7 Carregando dados na inicialização

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

Posso usar consultas LINQ-to-SQL semelhantes para criar métodos LoadFacts, LoadRecipes e LoadCocktails separados que possam ser usados depois da inicialização para carregar seus respectivos dados sob demanda.

Criar a interface do usuário

A página principal consiste em um Panorama com três PanoramaItems. O primeiro item consiste em uma ListBox que oferece um menu principal para o aplicativo. Quando o usuário seleciona um dos itens da ListBox, eu navego para a página correspondente, ou seja, a página de coleção de Recipes, Facts e Cocktails, ou a página Game. Pouco antes da navegação, eu carrego os dados correspondentes nas coleções Recipes, Facts ou Cocktails:

switch (CategoryList.SelectedIndex)
{
  case 0:
    App.ViewModel.LoadRecipes();
    NavigationService.Navigate(
      new Uri("/RecipesPage.xaml", UriKind.Relative));
    break;
 
... additional cases omitted for brevity
}

Quando o usuário seleciona um item na lista Seasonal Highlights na interface do usuário, eu examino o item selecionado para ver se é uma Recipe ou um Cocktail e navego para a página Recipe ou Cocktail individual, passando a ID do item como parte da cadeia de consulta de navegação, como mostra a Figura 8.

Figura 8 Selecionando na lista 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));

O usuário pode navegar do menu na página principal para uma das três páginas de listagem. Os dados de cada uma dessas página são associados a uma das coleções no ViewModel para exibir uma lista de itens: Recipes, Facts ou Cocktails. Cada uma dessas páginas oferece uma ListBox simples, onde cada item da lista contém um controle Imagem para a foto e um TextBlock para o nome do item. Por exemplo, a Figura 9 mostra a FactsPage.

Fun Facts, One of the Collection List Pages
Figura 9 Fun Facts, uma das páginas de lista de coleções

Quando o usuário seleciona um item individual nas listas Recipes, Facts ou Cocktails, eu navego para a página Recipe, Fact ou Cocktail individual, passando a ID do item individual na cadeia de consulta de navegação. Novamente, essas páginas são quase idênticas nos três tipos, cada uma delas oferecendo uma imagem e um texto abaixo. Observe que eu não defino um estilo explícito para os TextBlocks associados aos dados, mas todos eles usam TextWrapping=Wrap. Isso é feito com a declaração de um estilo de TextBlock no App.xaml.cs:

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

O efeito desse procedimento é que o TextBlock da solução que não define explicitamente o seu próprio estilo usará implicitamente esse outro estilo. A definição de um estilo implícito é outro novo recurso apresentado no Windows Phone SDK 7.1 como parte do Silverlight 4.

O code-behind de cada uma dessas páginas é simples. Na substituição de OnNavigatedTo, eu extraio a ID de item individual da cadeia de consulta, localizo esse item na coleção ViewModel e associo os dados a ele. O código da RecipePage é um pouco mais complexo do que os outros. O código adicional dessa página é todo relacionado ao HyperlinkButton posicionado no canto superior direito da página. Isso pode ser visto na Figura 10.

A Recipe Page with Pin Button
Figura 10 Uma página Recipe com o botão fixar

Mosaicos secundários

Quando o usuário clica no HyperlinkButton “fixar” na página Recipe individual, eu fixo esse item como um mosaico na home page do telefone. O ato de fixar leva o usuário à home page e desativa o aplicativo. Quando um mosaico é fixado dessa maneira, ele é animado periodicamente, invertendo frente e verso, como mostram a Figura 11 e a Figura 12.

Pinned Recipe Tile (Front)
Figura 11 Mosaico Recipe fixo (frente)

Pinned Recipe Tile (Back)
Figura 12 Mosaico Recipe fixo (verso)

Posteriormente, o usuário poderá tocar nesse mosaico fixo, navegando diretamente para esse item no aplicativo. Quando ele chegar à página, o botão “fixar” terá uma imagem “desafixar”. Se ele desafixar a página, ela será removida da home page e o aplicativo continuará.

Veja como isso funciona. Na substituição de OnNavigatedTo para a RecipePage, depois de executar o trabalho padrão para determinar a qual receita específica os dados devem ser associados, eu formulo uma cadeia de caracteres que posso usar posteriormente como URI dessa página:

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

No manipulador de clique do botão “fixar”, primeiro eu verifico se já existe um mosaico dessa página e, se não existir, crio um agora. Faço isso usando os dados da Recipe atuais: a imagem e o nome. Também defino uma única imagem estática (e um texto estático) para o verso do mosaico. Ao mesmo tempo, aproveito a oportunidade para redesenhar o próprio botão usando a imagem “desafixar”. Por outro lado, se o mosaico já existia, então eu devo estar no manipulador de clique, pois o usuário optou por desafixar o mosaico. Nesse caso, eu excluo o mosaico e redesenho o botão usando a imagem “fixar”, como mostra a Figura 13.

Figura 13 Fixando e desafixando páginas

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

Observe que, se o usuário tocar no mosaico fixado para ir à página Recipe e depois pressionar o botão Voltar no hardware do telefone, ele sairá do aplicativo. Isso pode ser confuso, pois o usuário geralmente espera sair do aplicativo apenas quando pressionar a opção Voltar na página principal, não em qualquer outra página. No entanto, a alternativa seria fornecer algum tipo de botão “página inicial” na página Recipe para permitir que o usuário navegue de volta para o restante do aplicativo. Infelizmente, isso também seria confuso, pois quando o usuário chegasse á página principal e pressionasse Voltar, ele voltaria à página Recipe fixa, em vez de sair do aplicativo. Por esse motivo, embora um mecanismo de “página inicial” não seja impossível de ser conseguido, é o comportamento que deve ser avaliado cuidadosamente antes da introdução.

Incorporar um XNA Game

Lembre-se de que eu originalmente criei o aplicativo como uma solução Windows Phone Silverlight and XNA Application. Isso me proporcionou três projetos. Venho trabalhando com o projeto MangoApp principal para criar a funcionalidade que não é de jogo. O projeto GameLibrary funciona como uma “ponte” entre o MangoApp do Silverlight e o XNA GameContent. Ele é referenciado no projeto MangoApp e, por sua vez, referencia o projeto GameContent. Isso não demanda mais nenhum trabalho. Existem duas tarefas principais necessárias para incorporar um jogo ao aplicativo para telefone:

  • Aprimorar a classe GamePage no projeto MangoApp para incluir toda a lógica do jogo.
  • Aprimorar o projeto GameContent para fornecer imagens e sons para o jogo (nenhuma alteração no código).

Verificando rapidamente os aprimoramentos que o Visual Studio gerou para um projeto que integra o Silverlight e o XNA, a primeira coisa a observar é que o App.xaml declara um SharedGraphicsDeviceManager. Ele gerencia o compartilhamento da tela entre os tempos de execução do Silverlight e do XNA. Esse objeto também é a única razão para a classe adicional AppServiceProvider no projeto. Essa classe é usada para armazenar em cache o gerenciador do dispositivo gráfico compartilhado, para que ele esteja disponível para o que for preciso no aplicativo, tanto Silverlight quanto XNA. A classe App possui um campo AppServiceProvider e também expõe algumas propriedades adicionais para integração do XNA: um ContentManager e um GameTimer. Eles são todos inicializados no novo método InitializeXnaApplication, juntamente com um GameTimer, que é usado para puxar a fila de mensagens do XNA.

O trabalho interessante é como integrar um jogo XNA em um aplicativo para telefone Silverlight. O jogo em si, na realidade é menos interessante. Portanto, neste exercício, em vez de gastar esforços escrevendo um jogo desde o início, vou adaptar um jogo existente, especificamente, o tutorial do jogo XNA sobre o AppHub: bit.ly/h0ZM4o.

Na minha adaptação, tenho uma coqueteleira, representada pela classe Player no código, que lança projéteis nas mangas que se aproximam (inimigas). Quando acerto uma manga, ela se abre e se transforma em um mangotini. Cada manga acertada acrescenta 100 pontos à pontuação. Cada vez que a coqueteleira colide com uma manga, a força do campo do jogador é reduzida em 10 pontos. Quando a força do campo chega a zero, o jogo termina. O usuário também pode encerrar o jogo a qualquer momento pressionando o botão Voltar do telefone, como esperado. A Figura 14 mostra o jogo em andamento.

The XNA Game in Progress
Figura 14 O jogo XNA em andamento

Não preciso fazer nenhuma alteração no (quase vazio) GamePage.xaml. Em vez disso, todo trabalho é feito no code-behind. O Visual Studio gera o código inicial para a classe GamePage, conforme descrito na Figura 15.

Figura 15 Código inicial da GamePage

Campo/Método Finalidade Alterações necessárias
ContentManager Carrega e gerencia o tempo de vida do conteúdo a partir do pipeline de conteúdo. Adicionar código para usar no carregamento de imagens e sons.
GameTimer No modelo de jogo XNA, o jogo executa ações quando os eventos Update e Draw são disparados, e esses eventos são governados por um temporizador. Inalterado.
SpriteBatch Usado para desenhar texturas no XNA. Adicionar código para usar no método Draw, para desenhar objetos do jogo (jogador, inimigos, projéteis, explosões, etc.).
Construtor de GamePage Cria um temporizador e conecta seus eventos Update e Draw aos métodos OnUpdate e OnDraw. Manter o código do temporizador e inicializar os objetos do jogo.
OnNavigatedTo Define o compartilhamento de gráficos entre o Silverlight e o XNA, e inicia o temporizador. Manter o código de compartilhamento e do temporizador, e carregar conteúdo no jogo, incluindo qualquer estado anterior do armazenamento isolado.
OnNavigatedFrom Para o temporizador e desativa o compartilhamento de gráficos do XNA. Manter o código do temporizador e do compartilhamento, e armazenar a pontuação do jogo e a integridade do jogador no armazenamento isolado.
OnUpdate (Vazio), controla o evento GameTimer.Update. Adicionar código para calcular as alterações em objetos do jogo (posição do jogador, número e posição dos inimigos, projéteis e explosões).
OnDraw (Vazio), controla o evento GameTimer.Draw. Adicionar código para desenhar objetos do jogo, a pontuação do jogo e a integridade do jogador.

O jogo é uma adaptação direta do tutorial do AppHub, que contém dois projetos: o projeto do jogo Shooter e o projeto de conteúdo ShooterContent. O conteúdo é composto de arquivos de imagem e de som. Embora isso não afete o código do aplicativo, eu posso alterá-los para que sejam alinhados ao tema da manga do meu aplicativo, e isso é apenas uma questão de substituir arquivos PNG e WAV. As alterações (código) necessárias estão todas no projeto do jogo Shooter. As diretrizes para a migração da classe Game para o Silverlight/XNA estão em AppHub: bit.ly/iHl3jz.

Em primeiro lugar, preciso copiar os arquivos do projeto do jogo Shooter para o meu projeto MangoApp existente. Além disso, eu copio os arquivos de conteúdo ShooterContent para o meu projeto GameContent existente. A Figura 16 resume as classes existentes no projeto do jogo Shooter.

Figura 16 Classes Game do Shooter

Classe Finalidade Alterações necessárias
Animation Anima os vários sprites no meu jogo: o jogador, objetos inimigos, projéteis e explosões. Eliminar GameTime.
Enemy Um sprite que representa os objetos inimigos que o usuário atira. Na minha adaptação, serão as mangas. Eliminar GameTime.
Game1 A classe de controle do jogo. Mesclar na classe GamePage.
ParallaxingBackground Anima as imagens do plano de fundo de nuvem para fornecer um efeito parallax tridimensional. Nenhuma.
Player Um sprite que representa o personagem do usuário no jogo. Na minha adaptação, será uma coqueteleira. Eliminar GameTime.
Program Usado somente se os jogo for para o Windows ou o Xbox. Não utilizado; pode ser excluído.
Projectile Um sprite que representa os projéteis que o jogador atira nos inimigos. Nenhuma.

Para incorporar este jogo no meu aplicativo para telefone, preciso fazer as seguintes alterações na classe GamePage:

  • Copiar todos os campos da classe Game1 para a classe GamePage. Também copiar a inicialização do campo no método Game1.Initialize para o construtor de GamePage.
  • Copiar o método LoadContent e todos os métodos para adicionar e atualizar inimigos, projéteis e explosões. Nenhum desses itens requer alterações.
  • Extrair todas as utilizações do GraphicsDeviceManager para usar uma propriedade GraphicsDevice em seu lugar.
  • Extrair o código nos métodos Game1.Update e Draw para os manipuladores de eventos do temporizador, GamePage.OnUpdate e OnDraw.

Um jogo XNA convencional cria um novo GraphicsDeviceManager, enquanto em um aplicativo para telefone, já existe um SharedGraphicsDeviceManager que expõe uma propriedade GraphicsDevice, e isso é tudo que eu realmente preciso. Para simplificar, vou armazenar em cache uma referência ao GraphicsDevice como um campo na classe GamePage.

Em um jogo XNA padrão, os métodos Update e Draw são substituições de métodos virtuais na classe base Microsoft.Xna.Framework.Game. No entanto, em um aplicativo Silverlight/XNA integrado, a classe GamePage não é derivada da classe Game do XNA, portanto, em vez disso, preciso abstrair o código de Update e Draw e inseri-lo nos manipuladores de eventos OnUpdate e OnDraw. Observe que algumas das classes de objeto do jogo (como Animation, Enemy e Player), os métodos Update e Draw, e alguns dos métodos auxiliares chamados por Update usam um parâmetro GameTime. Isso é definido no arquivo Microsoft.Xna.Framework.Game.dll e, em geral, ele deverá ser considerado um bug, se um aplicativo Silverlight contiver qualquer referência a esse assembly. O parâmetro GameTime pode ser completamente substituído pelas duas propriedades Timespan (TotalTime e ElapsedTime) expostas pelo objeto GameTimerEventArgs que é passado para os manipuladores de eventos OnUpdate e OnDraw do temporizador. Não considerando o GameTime, posso portar o código Draw inalterado.

O método Update original testa o estado do GamePad e chama condicionalmente Game.Exit. Esse procedimento não é usado no aplicativo Silverlight/XNA integrado, portanto, não deve ser portado para o novo método:

//if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed)
//{
//    // this.Exit();
//}

O novo método Update agora é um pouco mais do que uma proteção que chama outros métodos para atualizar os vários objetos do jogo. Eu atualizo o plano de fundo parallax, mesmo quando o jogo termina, mas só atualizo o jogador, os inimigos, as colisões, os projéteis e as explosões se o jogador ainda estiver vivo. Esses métodos auxiliares calculam o número e as posições dos vários objetos do jogo. Com a eliminação do uso de GameTime, todos eles podem ser portados inalterados, com uma exceção:

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

O método UpdatePlayer precisa de um pequeno ajuste. Na versão original do jogo, quando a integridade do jogador caía para zero, ela era redefinida para 100, ou seja, o jogo continuava indefinidamente. Na minha adaptação, quando a integridade do jogador cai para zero, eu defino um sinalizador como falso. Eu testo esse sinalizador nos métodos OnUpdate e OnDraw. Em OnUpdate, o valor do sinalizador determina se alterações adicionais devem ou não ser computadas para os objetos; em OnDraw, ele determina se devem ser apresentados objetos ou uma tela “game over” com a pontuação final:

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

Jogue também

Neste artigo, verificamos como desenvolver aplicativos em relação a vários dos novos recursos do Windows Phone SDK 7.1: bancos de dados locais, LINQ to SQL, mosaicos secundários e vinculação profunda, e integração com Silverlight/XNA. A versão 7.1 oferece muitos outros novos recursos e aprimoramentos para os recursos existentes. Para obter mais detalhes, consulte os seguintes links:

A versão final do aplicativo Mangolicious está disponível no Windows Phone Marketplace, em bit.ly/nuJcTA (observação: o software Zune é necessário para acessar). Observe que o exemplo utiliza o Silverlight for Windows Phone Toolkit (download grátis, disponível em bit.ly/qiHnTT).

Andrew Whitechapel atua como desenvolvedor há mais de 20 anos e atualmente trabalha como gerente de programa na equipe do Windows Phone, sendo responsável por aspectos principais da plataforma do aplicativo.

Agradecemos aos seguintes especialistas técnicos pela revisão deste artigo: Nick GravelynBrian HudsonHimadri Sarkar