Nota
El acceso a esta página requiere autorización. Puede intentar iniciar sesión o cambiar directorios.
El acceso a esta página requiere autorización. Puede intentar cambiar los directorios.
Creación de una aplicación “Mango”
Andrew Whitechapel
Descargar el ejemplo de código
“Mango” es el nombre de código interno para la versión Windows Phone SDK 7.1 y, por supuesto, el nombre de una deliciosa fruta tropical. Hay muchas maneras de usar los mangos; por ejemplo, en pasteles, ensaladas y en diversos tipos de cócteles. También se dice que el mango tiene muchos beneficios para la salud y que cuenta con una interesante historia cultural. En este artículo examinaré Mangolicious, una aplicación de Windows Phone SDK 7.1 acerca de los mangos. La aplicación brinda una variedad de recetas, cócteles y hechos sobre los mangos, pero el propósito real es explorar algunas de las nuevas e interesantes características de la versión 7.1, específicamente:
- Base de datos local y LINQ to SQL
- Mosaicos secundarios y vinculación en profundidad
- Integración de Silverlight/XNA
La experiencia del usuario en la aplicación es simple: la página principal ofrece una panorámica, con un menú en el primer elemento de la panorámica, una selección dinámica de las recetas y cócteles de la temporada actual en el segundo elemento y una simple información "acerca de" en el tercer elemento, tal como aparece en la figura 1.
Figura 1 Página principal de la panorámica de Mangolicious
Tanto el menú como los elementos en la sección Información destacada de la temporada actúan como vínculos para navegar a las demás páginas de la aplicación. La mayoría de las páginas son simples páginas de Silverlight y una página está dedicada a un juego XNA integrado. A continuación aparece un resumen de las tareas requeridas para crear esta aplicación de principio a fin:
- Crear la solución básica en Visual Studio.
- Crear de manera independiente la base de datos para los datos de recetas, cócteles y hechos.
- Actualizar la aplicación para consumir la base de datos y exponerla para enlace de datos.
- Crear las diversas páginas de UI y enlazarlas con los datos.
- Configurar la característica Mosaicos secundarios para permitir que el usuario ancle los elementos Receta en la página de inicio del teléfono.
- Incorporar un juego XNA en la aplicación.
Creación de la solución
Para esta aplicación, usaré la plantilla Aplicación XNA y Windows Phone Silverlight en Visual Studio. Con esto se genera una solución con tres proyectos; después de cambiar el nombre, se encuentran resumidos en la figura 2.
Figura 2 Proyectos en una solución XNA y Windows Phone Silverlight
Proyecto | Descripción |
MangoApp | Contiene la aplicación de teléfono misma, con una MainPage predeterminada y una GamePage secundaria. |
GameLibrary | Un proyecto esencialmente vacío que tiene todas las referencias correctas, pero nada de código. De manera crucial, incluye una referencia de contenido al proyecto Content (Contenido). |
GameContent | Un proyecto Content vacío, que contendrá todos los activos de juego (imágenes, archivos de sonido, etc.). |
Creación de la base de datos y de la clase DataContext
La versión Windows Phone SDK 7.1 introduce compatibilidad para bases de datos locales. Esto significa que una aplicación puede almacenar datos en un archivo de base de datos local (SDF) en el teléfono. El enfoque recomendado es crear la base de datos en el código, ya sea como parte de la misma aplicación o a través de una aplicación auxiliar independiente creada únicamente para crear la base de datos. Tiene sentido crear la base de datos dentro de la aplicación en escenarios en los que creará la mayoría o la totalidad de los datos solo cuando se ejecute la aplicación. En el caso de la aplicación Mangolicious, solo tengo datos estáticos y puedo rellenar la base de datos por adelantado.
Para hacerlo, crearé una aplicación auxiliar independiente para crear bases de datos, comenzando con la plantilla simple Aplicación de Windows Phone. Para crear la base de datos en código, necesito una clase derivada de DataContext, que está definida en la versión personalizada de teléfono del ensamblado System.Data.Linq. Esta misma clase DataContext se puede usar en la aplicación auxiliar que crea la base de datos y en la aplicación principal que consume la base de datos. En la aplicación auxiliar, debo especificar la ubicación de la base de datos que estará en almacenamiento aislado, porque es la única ubicación en que puedo escribir desde una aplicación de teléfono. La clase también contiene un conjunto de campos Table (tabla) para cada tabla de base de datos:
public class MangoDataContext : DataContext
{
public MangoDataContext()
: base("Data Source=isostore:/Mangolicious.sdf") { }
public Table<Recipe> Recipes;
public Table<Fact> Facts;
public Table<Cocktail> Cocktails;
}
Hay una asignación 1:1 entre las clases Table en el código y las tablas en la base de datos. Las propiedades Column (columna) se asignan a las columnas en la tabla en la base de datos e incluyen las propiedades de esquema de base de datos, como el tipo y tamaño de datos (INT, NVARCHAR, etc.), si la columna puede ser nula, si hay una columna clave, etc. Defino las clases Table para todas las demás tablas en la base de datos de la misma manera, tal como aparece en la figura 3.
Figure 3 Definición de las clases Table (Tabla)
[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
}
De todos modos, en la aplicación auxiliar, y usando un enfoque estándar Model-View-ViewModel (MVVM), ahora necesito una clase ViewModel para mediar entre la vista (la UI) y el modelo (los datos) a través del uso de la clase DataContext. El ViewModel tiene un campo DataContext y un conjunto de recopilaciones para los datos de tabla (Recetas, Hechos y Cócteles). Los datos son estáticos, por lo que aquí son suficientes las recopilaciones List<T> simples. Por la misma razón, solo necesito obtener descriptores de acceso de propiedades, no definir modificadores (consulte la figura 4).
Figura 4 Definición de propiedades de recopilación para datos de tabla en 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
}
También expongo un método público, al que puedo invocar desde la UI, para realmente crear la base de datos y todos los datos. En este método, creo la base de datos mismas si todavía no existe y luego creo cada tabla de una en una, rellenando cada una de ellas con datos estáticos. Por ejemplo, para crear la tabla Receta, creo varias instancias de la clase Recipe, que corresponden a filas de la tabla, agrego todas las filas de la recopilación a DataContext y, finalmente, enviar los datos a la base de datos. Se usa el mismo patrón para las tablas Hechos y Cócteles (consulte la figura 5).
Figura 5 Creación de la base de datos
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();
}
En un punto adecuado de la aplicación auxiliar, quizás en un controlador de clics de un botón, puedo invocar este método CreateDatabase. Cuando ejecuto la aplicación auxiliar (ya sea en el emulador o en un dispositivo físico), el archivo de base de datos se creará en el almacenamiento aislado de la aplicación. La tarea final es extraer ese archivo en el escritorio para poder usarlo en la aplicación principal. Para hacerlo, usaré la herramienta Explorador de almacenamiento aislado, una herramienta de línea de comandos que se incluye con Windows Phone SDK 7.1. El siguiente es el comando para capturar una instantánea del almacenamiento aislado desde el emulador al escritorio:
"C:\Program Files\Microsoft SDKs\Windows Phone\v7.1\Tools\IsolatedStorageExplorerTool\ISETool" ts xd {e0e7e3d7-c24b-498e-b88d-d7c2d4077a3b} C:\Temp\IsoDump
Este comando supone que la herramienta está instalada en una ubicación estándar. Los parámetros se explican en la figura 6.
Figura 6 Parámetros de línea de comandos del Explorador de almacenamiento aislado
Parámetro | Descripción |
ts | “Capturar instantánea” (el comando para descargar desde el almacenamiento aislado al escritorio). |
xd | Forma abreviada para XDE (es decir, el emulador). |
{e0e7e3d7-c24b-498e-b88d-d7c2d4077a3b} | El ProductID de la aplicación auxiliar. Aparece en el archivo WMAppManifest.xml y es diferente para cada aplicación. |
C:\Temp\IsoDump | Cualquier ruta de acceso válida en el escritorio en la que desea copiar la instantánea. |
Una vez que extraje el archivo SDF en el escritorio, ahora terminé con la aplicación auxiliar y puedo concentrarme en la aplicación Mangolicious que consumirá esta base de datos.
Consumo de la base de datos
En la aplicación Mangolicious, agrego el archivo SDF al proyecto y también agrego la misma clase DataContext personalizada a la solución, con un par de cambios menores. En Mangolicious no necesito escribir en la base de datos, por lo que puedo usarla directamente desde la carpeta de instalación de la aplicación. De este modo, la cadena de conexión es ligeramente diferente de la cadena de conexión de la aplicación auxiliar. Además, Mangolicious define una tabla SeasonalHighlights en el código. No hay una tabla SeasonalHighlights correspondiente en la base de datos. En lugar de eso, esta tabla de código extrae datos de dos tablas de base de datos subyacentes (Recetas y Cócteles) y se usa para rellenar el elemento de panorámica Información destacada de la temporada. Estos dos cambios son las únicas diferencias en la clase DataContext entre la aplicación auxiliar para la creación de base de datos y la aplicación que consume la base de datos de 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;
}
La aplicación Mangolicious también necesita una clase ViewModel y puedo usar la clase ViewModel desde la aplicación auxiliar como punto de partida. Necesito el campo DataContext y el conjunto de propiedades de recopilación List<T> para las tablas de datos. Además de eso, agregaré una propiedad de cadena para registrar la temporada actual, calculada en el constructor:
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";
}
El método crítico en ViewModel es el método LoadData. Aquí inicializo la base de datos y realizo consultas LINQ-to-SQL para cargar los datos a través de DataContext en mis recopilaciones en memoria. En este punto podría cargar previamente las tres tablas, pero deseo optimizar el rendimiento de inicio retrasando la carga de datos a menos y hasta que la página pertinente se visite realmente. Los únicos datos que debo cargar en el inicio son los datos para la tabla SeasonalHighlight, porque aparece en la página principal. Para esto, tengo dos consultas para seleccionar solo filas de las tablas Recetas y Cócteles que coinciden con la temporada actual y agregar los conjuntos de filas combinadas a la recopilación, tal como se muestra en la figura 7.
Figura 7 Carga de datos en el inicio
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;
}
Puedo usar consultas LINQ-to-SQL similares para crear métodos LoadFacts, LoadRecipes y LoadCocktails independientes que se pueden usar después del inicio para cargar los datos respectivos a petición.
Creación de la UI
La página principal consta de una panorámica con tres PanoramaItems. El primer elemento consiste en un ListBox que ofrece un menú principal para la aplicación. Cuando el usuario selecciona uno de los elementos de ListBox, navego a la página correspondiente, es decir, a la página de recopilación para Recetas, Hechos o Cócteles o a la página Juego. Justo antes de navegar, me aseguro de cargar los datos correspondientes en las recopilaciones Recetas, Hechos o Cócteles:
switch (CategoryList.SelectedIndex)
{
case 0:
App.ViewModel.LoadRecipes();
NavigationService.Navigate(
new Uri("/RecipesPage.xaml", UriKind.Relative));
break;
... additional cases omitted for brevity
}
Cuando el usuario selecciona un elemento desde la Información destacada de la temporada en la UI, examino el elemento seleccionado para ver si es una receta o un cóctel y luego navego a la página Receta o Cóctel individual y transfiero el identificador del elemento como parte de la cadena de consultas de navegación, tal como se muestra en la figura 8.
Figura 8 Selección desde la lista Información destacada de la temporada
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));
El usuario puede navegar desde el menú de la página principal a una de las tres páginas listadas. Cada una de estas páginas se enlaza a los datos de una de las recopilaciones en el ViewModel para mostrar una lista de elementos: Recetas, Hechos o Cócteles. Cada una de estas páginas ofrece un ListBox simple en el que cada elemento de la lista contiene un control Image para la fotografía y un TextBlock para el nombre del elemento. Por ejemplo, la figura 9 muestra la FactsPage.
Figura 9 Anécdotas, una de las páginas de la lista de recopilaciones
Cuando el usuario selecciona un elemento individual para las listas Recetas, Hechos o Cócteles, navego a la página Receta, Hecho o Cóctel individual y transmito el identificador del elemento individual en la cadena de consultas de navegación. Nuevamente, estas páginas son casi idénticas en los tres tipos, donde cada una ofrece una imagen y algo de texto abajo. Observe que no defino un estilo explícito para los TextBlocks enlazados a los datos, pero que ellos de todos modos usan TextWrapping=Wrap. Esto se realiza al declarar un estilo TextBlock en el App.xaml.cs:
<Style TargetType="TextBlock" BasedOn="{StaticResource
PhoneTextNormalStyle}">
<Setter Property="TextWrapping" Value="Wrap"/>
</Style>
El efecto de esto es que cualquier TextBlock de la solución que no define explícitamente su propio estilo usará en su lugar implícitamente este. El estilo implícito es otra característica nueva presentada en Windows Phone SDK 7.1 como parte de Silverlight 4.
El código subyacente para cada una de estas páginas es simple. En el reemplazo de OnNavigatedTo, extraigo el identificador del elemento individual desde la cadena de consultas, encuentro ese elemento en la recopilación ViewModel y enlazo datos a él. El código para RecipePage es un poco más complejo que los demás: el código adicional de esta página está completamente relacionado con el HyperlinkButton ubicado en la esquina superior derecha de la página. Esto se puede observar en la figura 10.
Figura 10 Una página de receta con botón de anclaje
Mosaicos secundarios
Cuando el usuario hace clic en el HyperlinkButton de “anclaje” en la página Receta individual, anclo este elemento como mosaico en la página de inicio del teléfono. El acto de anclar lleva al usuario a la página de inicio y desactiva la aplicación. Cuando un mosaico se ancla de esta manera, se anima periódicamente, volteando hacia adelante y hacia atrás, tal como aparece en la figura 11 y en la figura 12.
Figura 11 Mosaico de receta anclado (de frente)
Figura 12 Mosaico de receta anclado (por detrás)
Posteriormente, el usuario puede pulsar este mosaico anclado, con lo que navega directamente a ese elemento dentro de la aplicación. Cuando llega a la página, el botón de “anclar” ahora tendrá una imagen de “desanclar”. Si desancla la página, se quitará de la página de inicio y la aplicación continúa.
Así funciona. En el reemplazo de OnNavigatedTo para RecipePage, después de realizar el trabajo estándar para determinar a qué receta específica están enlazados los datos, formulo una cadena que puedo usar más adelante como el URI de esta página:
thisPageUri = String.Format("/RecipePage.xaml?ID={0}", recipeID);
En el controlador de clics del botón para el botón "anclar", primero compruebo si ya existe un mosaico para esta página y, si no es así, lo creo ahora. Creo el mosaico usando los datos actuales de la receta: la imagen y el nombre. También defino una imagen estática, y un texto estático, para la parte posterior del mosaico. Al mismo tiempo, aprovecho la oportunidad para volver a pintar el botón, usando la imagen “desanclar”. Por otro lado, si el mosaico ya existe, debo estar en el controlador de clics porque el usuario ha elegido desanclar el mosaico. En este caso, elimino el mosaico y vuelvo a pintar el botón usando la imagen “anclar”, tal como aparece en la figura 13.
Figura 13 Anclar y desanclar 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 si el usuario pulsa el mosaico anclado para ir a la página Receta y luego presiona el botón Atrás del hardware del teléfono, saldrá completamente de la aplicación. Esto puede resultar confuso, puesto que el usuario normalmente espera salir de la aplicación solo cuando presiona Atrás en la página principal y no desde ninguna otra página. Sin embargo, la alternativa sería proporcionar alguna especie de botón “inicio” en la página Receta para permitir que el usuario pueda navegar de vuelta al resto de la aplicación. Lamentablemente, esto también sería confuso, puesto que cuando el usuario llegase a la página principal y presionara Atrás, volvería a la página Receta anclada en lugar de salir de la aplicación. Por esta razón, a pesar de que un mecanismo de “inicio” se puede obtener, por su comportamiento es algo que debiese pensarse cuidadosamente antes de introducir.
Incorporación de un juego XNA
Recuerde que originalmente creé la aplicación como una solución de Windows Phone Silverlight y aplicación XNA. Esto me generó tres proyectos. He estado trabajando con el proyecto principal MangoApp para crear la funcionalidad que no se refiere al juego. El proyecto GameLibrary actúa como un “puente” entre la MangoApp de Silverlight MangoApp y el GameContent de XNA. Se hace referencia a ese proyecto en el proyecto MangoApp y, a su vez, hace referencia al proyecto GameContent. No necesita más trabajo. Existen dos tareas principales requeridas para incorporar un juego en mi aplicación de teléfono:
- Mejorar la clase GamePage en el proyecto MangoApp para incluir toda la lógica del juego.
- Mejorar el proyecto GameContent para proporcionar imágenes y sonidos para el juego (sin cambios de código).
Si miramos brevemente las mejoras que Visual Studio generó para un proyecto que integra Silverlight y XNA, lo primero que se observa es que App.xaml declara un ShareGraphicsDeviceManager. Esto administra el uso compartido de la pantalla entre los tiempos de ejecución de Silverlight y XNA. Este objeto es también la única razón para la clase AppServiceProvider adicional en el proyecto. Esta clase se usa para almacenar en caché el administrador de dispositivos gráficos compartido para que esté disponible para cualquier elemento que lo necesite en la aplicación, ya sea Silverlight o XNA. La clase App tiene un campo AppServiceProvider y también expone algunas propiedades adicionales para la integración de XNA: un ContentManager y un GameTimer. Todos se inicializan en el nuevo método InitializeXnaApplication, junto con un GameTimer, que se usa para bombear la cola de mensajes de XNA.
El trabajo interesante es cómo integrar un juego XNA dentro de una aplicación de teléfono de Silverlight. El juego en sí mismo es, en realidad, menos interesante. Por lo tanto, para este ejercicio, en lugar de esforzarse en escribir un juego completo desde cero, adaptaré un juego existente; específicamente, el tutorial de juego de XNA en AppHub: bit.ly/h0ZM4o.
En mi adaptación, tengo una coctelera, presentada en el código por la clase Player, que dispara proyectiles a los mangos que se acercan (enemigos). Cuando golpeo un mango, se rompe y se transforma en un mangotini. Cada mango que se golpee suma 100 en la puntuación. Cada vez que la coctelera choca con un mango, la fuerza del campo del jugador disminuye en 10. Cuando la fuerza del campo llega a cero, el juego se termina. El usuario también puede finalizar el juego en cualquier momento si presiona el botón Atrás del teléfono, tal como podría esperarse. La figura 14 muestra el juego en curso.
Figura 14 El juego XNA en curso
No es necesario realizar ningún cambio en el (casi vacío) GamePage.xaml. En lugar de eso, todo el trabajo se realiza en el código subyacente. Visual Studio genera código de inicio para esta clase GamePage, tal como se describe en la figura 15.
Figura 15 Código de inicio de GamePage
Campo/método | Propósito | Cambios requeridos |
ContentManager | Carga y administra la duración del contenido desde la canalización de contenido. | Agregar código para usar esto para cargar imágenes y sonidos. |
GameTimer | En el modelo de juego XNA, el juego realiza acciones cuando se activan los eventos Update y Draw y a estos eventos los rige un temporizador. | Sin cambios. |
SpriteBatch | Se usa para dibujar texturas en XNA. | Agregar código para usar esto en el método Draw para dibujar los objetos del juego (jugador, enemigo, proyectiles, explosiones, etc.). |
Constructor GamePage | Crea un temporizador y enlaza sus eventos Update y Draw a los métodos OnUpdate y OnDraw. | Mantener el código del temporizador y, adicionalmente, inicializar los objetos del juego. |
OnNavigatedTo | Configura el uso compartido de los gráficos entre Silverlight y XNA e inicia el temporizador. | Mantener el código del temporizador y uso compartido y, adicionalmente, cargar contenido en el juego, incluido cualquier estado anterior desde el almacenamiento aislado. |
OnNavigatedFrom | Detiene el temporizador y desactiva el uso compartido de gráficos de XNA. | Mantener el código del temporizador y uso compartido y, adicionalmente, almacenar la puntuación y el estado del jugador en el almacenamiento aislado. |
OnUpdate | (Vacío), controla el evento GameTimer.Update. | Agregar código para calcular los cambios de los objetos del juego (posición del jugador, número y posición de los enemigos, proyectiles y explosiones). |
OnDraw | (Vacío), controla el evento GameTimer.Draw. | Agregar código para dibujar objetos del juego, la puntuación del juego y el estado del jugador. |
El juego es una adaptación directa del tutorial de AppHub, que contiene dos proyectos: el proyecto de juego Shooter y el proyecto de contenido ShooterContent. El contenido contiene imágenes y archivos de sonido. A pesar de que no afecta el código de la aplicación, puedo cambiarlos para alinearlos con el tema de mango de mi aplicación y para eso solo se necesita reemplazar los archivos PNG y WAV. Los cambios requeridos (código) se encuentran todos en el proyecto de juego Shooter. En AppHub hay pautas para migrar de la clase Game a Silverlight/XNA: bit.ly/iHl3jz.
Primero, debo copiar los archivos del proyecto de juego Shooter en mi proyecto MangoApp existente. Además, copio los archivos de contenido ShooterContent en mi proyecto GameContent existente. La figura 16 resume las clases existentes en el proyecto de juego Shooter.
Figure 16 Clases de juego Shooter
Clase | Propósito | Cambios requeridos |
Animation | Anima los diversos sprites de mi juego: el jugador, los objetos enemigos, los proyectiles y las explosiones. | Eliminar GameTime. |
Enemy | Un sprite que representa los objetos enemigos a los que el usuario dispara. En mi adaptación, mangos. | Eliminar GameTime. |
Game1 | La clase de control del juego. | Combinar con la clase GamePage. |
ParallaxingBackground | Anima las imágenes de nube en segundo plano para brindar un efecto parallax en 3D. | Ninguno. |
Player | Un sprite que representa el personaje del usuario en el juego. En mi adaptación, una coctelera. | Eliminar GameTime. |
Programa | Solo se usa si el juego es para Windows o Xbox. | No se usa, se puede eliminar. |
Projectile | Un sprite que representa los proyectiles que el jugador dispara a los enemigos. | Ninguno. |
Para incorporar este juego en mi aplicación de teléfono, es necesario realizar los siguientes cambios en la clase GamePage:
- Copiar todos los campos de la clase Game1 a la clase GamePage. También copiar la inicialización de campo en el método Game1.Initialize en el constructor GamePage.
- Copiar el método LoadContent y todos los métodos para agregar y actualizar enemigos, proyectiles y explosiones. Ninguno de estos requiere cambios.
- Extraer todos los usos de GraphicsDeviceManager para, en su lugar, usar una propiedad GraphicsDevice.
- Extraer el código de los métodos Game1.Update y Draw en los controladores de eventos de temporizador GamePage.OnUpdate y OnDraw.
Un juego XNA convencional crea un nuevo GraphicsDeviceManager, mientras que en una aplicación de teléfono ya tendré un SharedGraphicsDeviceManager que expone una propiedad GraphicsDevice y eso es todo lo que realmente necesito. Para simplificar las cosas, almacenaré en caché una referencia a GraphicsDevice como un campo en una clase GamePage.
En un juego XNA estándar, los métodos Update y Draw son reemplazos de métodos virtuales en la clase Microsoft.Xna.Framework.Game de base. Sin embargo, en una aplicación Silverlight/XNA integrada, la clase GamePage no se deriva de la clase XNA Game, por lo que debo extraer el código de Update y Draw y en su lugar instalarlo en los controladores de eventos OnUpdate y OnDraw. Observe que algunas de las clases de objeto de juego (como Animation, Enemy y Player), los métodos Update y Draw y algunos de los métodos auxiliares que Update llama toman un parámetro GameTime. Esto se define en Microsoft.Xna.Framework.Game.dll y generalmente debiera ser considerado como un error si una aplicación Silverlight contiene alguna referencia a este ensamblado. El parámetro GameTime se puede reemplazar completamente con las dos propiedades Timespan (TotalTime y ElapsedTime) expuestas desde el objeto GameTimerEventArgs que se transmite a los controladores de eventos de temporizador OnUpdate y OnDraw. Además de GameTime, puedo transporta el código Draw sin cambios.
El método Update original prueba el estado de GamePad y llama condicionalmente a Game.Exit. Esto no se usa en una aplicación Silverlight/XNA integrada y, por lo tanto, no se debe transportar al método nuevo:
//if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed)
//{
// // this.Exit();
//}
El nuevo método Update es ahora poco más que un agente que se llama en otros métodos para actualizar los diversos objetos de juego. Actualizo el segundo plano de parallax incluso cuando el juego ha terminado, pero solo actualizo el jugador, los enemigos, la colisión, los proyectiles y las explosiones si el jugador sigue vivo. Estos métodos auxiliares calculan el número y las posiciones de los diversos objetos del juego. Una vez eliminado el uso de GameTime, se pueden transportar sin cambios, con una sola excepción:
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);
}
}
El método UpdatePlayer sí necesita un pequeño ajuste. En la versión original del juego, cuando el estado del jugador llega a cero, se restablecía en 100, lo que implicaba que el juego realizaba un bucle eterno. En mi adaptación, cuando el estado del jugador cae a cero, defino una marca en falso. Pruebo esta marca en el método OnUpdate y en OnDraw. En OnUpdate, el valor de la marca determina si calcular o no más cambios en los objetos; en OnDraw, determina si dibujar los objetos o si dibujar una pantalla de "juego terminado" con la puntuación 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;
}
}
Seguir la corriente
En este artículo, examinamos cómo desarrollar aplicaciones con varias de las características nuevas de Windows Phone SDK 7.1: bases de datos locales, LINQ to SQL, mosaicos secundarios y vinculación en profundidad e integración Silverlight/XNA. La versión 7.1 ofrece muchísimas características y mejoras nuevas a las características existentes. Para obtener detalles existentes, consulte los siguientes vínculos:
- Novedades de Windows Phone SDK: bit.ly/c2RmNr
- Mosaicos: bit.ly/oQlu15
- Combinación de Silverlight y XNA: bit.ly/p4RncQ
- Introducción a la base de datos local para Windows Phone: bit.ly/l23UQM
La versión final de la aplicación Mangolicious se encuentra disponible en el Catálogo de soluciones de Windows Phone en bit.ly/nuJcTA (nota: se necesita el software Zune para obtener acceso). Observe que la muestra usa Silverlight para el kit de herramientas de Windows Phone (una descarga gratuita disponible en bit.ly/qiHnTT).
Andrew Whitechapel* lleva más de 20 años como desarrollador y actualmente trabaja como jefe de programas en el equipo de Windows Phone, responsable de partes centrales en la plataforma de aplicaciones.*
Gracias a los siguientes expertos técnicos por su ayuda en la revisión de este artículo: Nick Gravelyn, Brian Hudson y Himadri Sarkar