This article is currently in the process of being translated into Spanish (~99% done).
In this article series, we're building a complete Snake game from scratch. It makes sense to start with the Introduction and then work your way through the articles one by one, to get the full understanding.
If you want to get the complete source code for the game at once, to get started modifying and learning from it right now, consider downloading all our samples!
Improving SnakeWPF: Adding a high score list
En el artículo anterior, realizamos muchas mejoras visuales en nuestra implementación SnakeWPF. En este artículo, me gustaría agregar una característica muy interesante: ¡Una lista de puntuaciones altas! Además, me gustaría hacer que el juego sea un poco más fácil de usar, agregando una pantalla de bienvenida . También estaré reemplazando el muy no-gamey "Murió" -messagebox con una pantalla en el juego.
Necesitamos bastante código y marcas adicionales para esto, pero comencemos con la parte fácil: ¡el XAML!
XAML
Lo primero que me gustaría hacer es agregar un montón de XAML a la ventana de Serpiente (Snake). Esto consistirá principalmente en 4 contenedores nuevos (en este caso, controles fronterizos), que albergarán una variedad de controles secundarios para soportar diversas situaciones:
- Un contenedor para mostrar un mensaje de bienvenida cuando comienza el juego, informando sobre los controles que se utilizarán, etc.
- Un contenedor para mostrar la lista de puntuación alto.
- Un contenedor para mostrar cuando el usuario ha superado una de las puntuaciones más altas, incluido un TextBox para ingresar un nombre
- Un contenedor para mostrar cuando el usuario muere, pero no ha llegado a la lista de puntuación alto (reemplazando el aburrido MessageBox que usamos anteriormente)
Agregaremos estos contenedores al lienzo GameArea y luego simplemente los ocultaremos cuando no los necesitemos. Como se mencionó, cada contenedor tendrá un propósito diferente e incluirá un poco de marcado, pero solo usamos controles WPF que ya se han discutido en este tutorial.
Mensaje de bienvenida
Agregue esta pieza de XAML dentro del control del área del juego:
<Border BorderBrush="Silver" BorderThickness="2" Width="300" Height="300" Canvas.Left="50" Canvas.Top="50" Name="bdrWelcomeMessage" Panel.ZIndex="1">
<StackPanel Orientation="Vertical" Background="AliceBlue">
<TextBlock FontWeight="Bold" FontSize="50" HorizontalAlignment="Center" Margin="0,20,0,0">SnakeWPF</TextBlock>
<TextBlock TextWrapping="Wrap" Margin="20" FontSize="16">Use the Arrow keys to control the green snake. Make it eat the red apples, but be sure not to crash into the walls or the tail of the snake!</TextBlock>
<TextBlock FontWeight="Bold" HorizontalAlignment="Center" FontSize="24" Foreground="Maroon">Press SPACE to start!</TextBlock>
<Button Margin="20" Name="btnShowHighscoreList" Click="BtnShowHighscoreList_Click" HorizontalAlignment="Center" Padding="10,3">Show High Score List...</Button>
</StackPanel>
</Border>
Le dice brevemente al usuario de qué se trata el juego, cómo se controla la serpiente y cómo iniciar el juego. El borde, que contiene todo el contenido, es inicialmente visible, por lo que esto será lo primero que el usuario conocerá cuando comience el juego. En la parte inferior de la pantalla, agregué un botón para mostrar la lista de puntuaciones altas (que agregaremos en solo un minuto). El controlador de eventos Click se implementará en el Código subyacente más adelante.
Lista de puntuación alta
Ahora se vuelve un poco más complejo, porque quiero hacer esto a la manera WPF y usar el enlace de datos para mostrar la lista de puntuación alta en lugar de, por ejemplo creación y actualización manual de la lista. Pero no se preocupe, lo explicaré todo a medida que avancemos. Primero, agregue esta pieza de XAML dentro del GameArea Canvas, como lo hicimos antes: el Canvas, como se mencionó, mantendrá todos nuestros controles Border, cada uno de ellos ofreciendo su propia funcionalidad para nuestro juego:
<Border BorderBrush="Silver" BorderThickness="2" Width="300" Height="300" Canvas.Left="50" Canvas.Top="50" Name="bdrHighscoreList" Panel.ZIndex="1" Visibility="Collapsed">
<StackPanel Orientation="Vertical" Background="AliceBlue">
<Border BorderThickness="0,0,0,2" BorderBrush="Silver" Margin="0,10">
<TextBlock HorizontalAlignment="Center" FontSize="34" FontWeight="Bold">High Score List</TextBlock>
</Border>
<ItemsControl ItemsSource="{Binding Source={StaticResource HighScoreListViewSource}}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<DockPanel Margin="7">
<TextBlock Text="{Binding PlayerName}" DockPanel.Dock="Left" FontSize="22"></TextBlock>
<TextBlock Text="{Binding Score}" DockPanel.Dock="Right" FontSize="22" HorizontalAlignment="Right"></TextBlock>
</DockPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</Border>
Observe cómo este borde no se muestra inicialmente (Visibilidad = contraído). Utilizamos un ItemsControl (ya hablamos de esto anteriormente en este tutorial), con un ItemSource personalizado llamado HighScoreListViewSource . Utilizaremos un CollectionViewSource para asegurarnos de que la colección a la que nos unimos esté siempre ordenada correctamente. Necesitamos definir este recurso en el XAML de Windows, por lo que como hijo de la etiqueta de Windows, debe agregar este marcado, haciendo que su declaración de Windows se vea así:
<Window x:Class="WpfTutorialSamples.Games.SnakeWPFSample"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:WpfTutorialSamples.Games"
mc:Ignorable="d"
x:Name="window"
xmlns:scm="clr-namespace:System.ComponentModel;assembly=WindowsBase"
Title="SnakeWPF - Score: 0" SizeToContent="WidthAndHeight" ContentRendered="Window_ContentRendered" KeyUp="Window_KeyUp"
ResizeMode="NoResize" WindowStyle="None" Background="Black" MouseDown="Window_MouseDown">
<Window.Resources>
<CollectionViewSource Source="{Binding ElementName=window, Path=HighscoreList}" x:Key="HighScoreListViewSource">
<CollectionViewSource.SortDescriptions>
<scm:SortDescription Direction="Descending" PropertyName="Score" />
</CollectionViewSource.SortDescriptions>
</CollectionViewSource>
</Window.Resources>
................
Tenga en cuenta que introduje una nueva referencia: xmlns: scm , utilizada para acceder al tipo SortDescription. También agregué la propiedad x: Name y la configuré en window , para que podamos hacer referencia a los miembros definidos en la clase MainWindow en código subyacente.
En Window.Resources, he agregado un nuevo CollectionViewSource . Utiliza el enlace para adjuntar a una propiedad llamada HighscoreList , que definiremos en código subyacente. Observe también que le agrego una SortDescription, especificando que la lista debe clasificarse descendiente por una propiedad llamada Puntuación, básicamente significa que la puntuación más alta se mostrará primero y así sucesivamente.
En código subyacente, necesitamos definir la propiedad llamada HighscoreList , en la que se basa ItemsSource, pero llegaremos a eso una vez que hayamos terminado de agregar el último XAML.
Nuevo récord
Cuando el usuario supera una puntuación alta existente, mostraremos un mensaje agradable al respecto. El XAML se ve así, y una vez más, debe agregarse dentro del lienzo de GameArea:
<Border BorderBrush="Silver" BorderThickness="2" Width="300" Height="300" Canvas.Left="50" Canvas.Top="50" Name="bdrNewHighscore" Panel.ZIndex="1" Visibility="Collapsed">
<StackPanel Orientation="Vertical" Background="AliceBlue">
<TextBlock HorizontalAlignment="Center" FontSize="34" FontWeight="Bold" Margin="20">New Highscore!</TextBlock>
<TextBlock HorizontalAlignment="Center" TextWrapping="Wrap" FontSize="16">
Congratulations - you made it into the SnakeWPF highscore list! Please enter your name below...
</TextBlock>
<TextBox Name="txtPlayerName" FontSize="28" FontWeight="Bold" MaxLength="8" Margin="20" HorizontalContentAlignment="Center"></TextBox>
<Button Name="btnAddToHighscoreList" FontSize="16" HorizontalAlignment="Center" Click="BtnAddToHighscoreList_Click" Padding="10,5">Add to highscore list</Button>
</StackPanel>
</Border>
Todo muy simple con algo de texto, un cuadro de texto para ingresar el nombre y un botón para hacer clic para agregar a la lista; definiremos el controlador de eventos BtnAddToHighscoreList_Click más adelante.
"Oh no, ¡has muerto!"
La última parte es la pantalla "Oh, no, has muerto y no llegaste a la lista de puntuaciones más altas", que usaremos para reemplazar el aburrido MessageBox que hizo lo mismo anteriormente. El XAML se ve así:
<Border BorderBrush="Silver" BorderThickness="2" Width="300" Height="300" Canvas.Left="50" Canvas.Top="50" Name="bdrEndOfGame" Panel.ZIndex="1" Visibility="Collapsed">
<StackPanel Orientation="Vertical" Background="AliceBlue">
<TextBlock HorizontalAlignment="Center" FontSize="40" FontWeight="Bold" Margin="0,20,0,0">Oh no!</TextBlock>
<TextBlock HorizontalAlignment="Center" FontSize="26" FontWeight="Bold">...you died!</TextBlock>
<TextBlock Margin="20" TextAlignment="Center" FontSize="16">Your score: </TextBlock>
<TextBlock Name="tbFinalScore" TextAlignment="Center" FontSize="48" FontWeight="Bold" Foreground="Maroon">0</TextBlock>
<TextBlock TextAlignment="Center" FontSize="16" Margin="20">Press SPACE to start a new game!</TextBlock>
</StackPanel>
</Border>
Informa al usuario sobre el desafortunado giro de los eventos, muestra la puntuación final y le dice al usuario cómo comenzar un nuevo juego, ¡bastante simple!
Código subyacente
Con todo el XAML en su lugar, ¡finalmente estamos listos para implementar el código subyacente! Primero, necesitamos implementar los controladores de eventos que definimos en XAML. Aquí está el botón "Mostrar lista de puntuación alta":
private void BtnShowHighscoreList_Click(object sender, RoutedEventArgs e)
{
bdrWelcomeMessage.Visibility = Visibility.Collapsed;
bdrHighscoreList.Visibility = Visibility.Visible;
}
Muy simple, como puede ver, cuando se hace clic en el botón, ocultamos el mensaje de bienvenida y luego mostramos la lista de puntuación alta, lo agregaremos ahora.
Implementando la lista de puntuación alta
El otro controlador de eventos que tenemos se relaciona con agregar una nueva entrada a la lista de puntuación alta, pero para eso, necesitamos un par de otras adiciones, en primer lugar, la propiedad real para mantener las entradas de puntuación alta:
public ObservableCollection<SnakeHighscore> HighscoreList
{
get; set;
} = new ObservableCollection<SnakeHighscore>();
Como puede ver, esta es una ObservableCollection , que contiene el tipo SnakeHighscore . Primero, asegúrese de incluir el espacio de nombres que contiene el tipo ObservableCollection:
using System.Collections.ObjectModel;
Luego implemente la clase SnakeHighscore :
public class SnakeHighscore
{
public string PlayerName { get; set; }
public int Score { get; set; }
}
Como puede ver, es una clase bastante simple: solo sirve como contenedor para el nombre y la puntuación del jugador que llegó a la lista de puntuación altas.
Cargando / guardando la lista de puntuación alta
También necesitamos un código para cargar y guardar la lista: se llamará al método Guardar cuando se agregue una nueva entrada a la lista, mientras que se llama al método Cargar cuando nuestro El juego comienza. Usaré un archivo XML simple para mantener la lista, lo que nos permitirá usar la clase XmlSerializer incorporada para cargar y guardar la lista automáticamente.
Hay MUCHAS formas de cargar / guardar datos, y varios otros formatos relevantes como JSON o incluso un archivo de texto sin formato, pero quería mantener esta parte en la menor cantidad posible de líneas de código, ya que no es tan relevante para un tutorial de WPF. Además, el enfoque XmlSerializer hace que el código sea bastante flexible: puede agregar fácilmente nuevas propiedades a la clase SnakeHighscore y se mantendrán automáticamente. Aquí está el método LoadHighscoreList () :
private void LoadHighscoreList()
{
if(File.Exists("snake_highscorelist.xml"))
{
XmlSerializer serializer = new XmlSerializer(typeof(List<SnakeHighscore>));
using(Stream reader = new FileStream("snake_highscorelist.xml", FileMode.Open))
{
List<SnakeHighscore> tempList = (List<SnakeHighscore>)serializer.Deserialize(reader);
this.HighscoreList.Clear();
foreach(var item in tempList.OrderByDescending(x => x.Score))
this.HighscoreList.Add(item);
}
}
}
Debe incluir un par de espacios de nombres adicionales para esto:
using System.IO;
using System.Xml.Serialization;
Asegúrese de llamar al método LoadHighscoreList (), por ejemplo en el constructor de la ventana:
public SnakeWPFSample()
{
InitializeComponent();
gameTickTimer.Tick += GameTickTimer_Tick;
LoadHighscoreList();
}
A continuación, implementamos el método SaveHighscoreList () :
private void SaveHighscoreList()
{
XmlSerializer serializer = new XmlSerializer(typeof(ObservableCollection<SnakeHighscore>));
using(Stream writer = new FileStream("snake_highscorelist.xml", FileMode.Create))
{
serializer.Serialize(writer, this.HighscoreList);
}
}
El método Save es más relevante para llamar cuando agregamos una nueva entrada; esto sucede en el controlador de eventos BtnAddToHighscoreList_Click () , que debería verse así:
private void BtnAddToHighscoreList_Click(object sender, RoutedEventArgs e)
{
int newIndex = 0;
// Where should the new entry be inserted?
if((this.HighscoreList.Count > 0) && (currentScore < this.HighscoreList.Max(x => x.Score)))
{
SnakeHighscore justAbove = this.HighscoreList.OrderByDescending(x => x.Score).First(x => x.Score >= currentScore);
if(justAbove != null)
newIndex = this.HighscoreList.IndexOf(justAbove) + 1;
}
// Create & insert the new entry
this.HighscoreList.Insert(newIndex, new SnakeHighscore()
{
PlayerName = txtPlayerName.Text,
Score = currentScore
});
// Make sure that the amount of entries does not exceed the maximum
while(this.HighscoreList.Count > MaxHighscoreListEntryCount)
this.HighscoreList.RemoveAt(MaxHighscoreListEntryCount);
SaveHighscoreList();
bdrNewHighscore.Visibility = Visibility.Collapsed;
bdrHighscoreList.Visibility = Visibility.Visible;
}
Es bastante simple: tratamos de decidir si la nueva entrada debe agregarse en la parte superior de la lista (¡un nuevo mejor!) O si pertenece más abajo en la lista. Una vez que tenemos el nuevo índice, insertamos una nueva instancia de la clase SnakeHighscore , usando la puntuación actual y el nombre ingresado por el jugador. Luego eliminamos las entradas no deseadas del final de la lista, si la lista de repente tiene más elementos de los que queremos (MaxHighscoreListEntryCount). Luego guardamos la lista ( SaveHighscoreList () ) y ocultamos el contenedor bdrNewHighscore , cambiando la vista al contenedor bdrHighscoreList .
Pero todavía hay un par de cosas que hacer. En primer lugar, estas nuevas pantallas (mensaje muerto, lista de puntuaciones altas, etc.) deben ocultarse cada vez que se mira un nuevo juego. Entonces, la parte superior del método StartNewGame (), que implementamos en un artículo anterior, ahora debería verse así:
private void StartNewGame()
{
bdrWelcomeMessage.Visibility = Visibility.Collapsed;
bdrHighscoreList.Visibility = Visibility.Collapsed;
bdrEndOfGame.Visibility = Visibility.Collapsed;
........
Lo siguiente que debemos hacer es modificar el método EndGame () . En lugar de solo mostrar el cuadro de mensaje, debemos verificar si el usuario acaba de ingresar a la lista de puntuación alta o no y luego mostrar el contenedor de mensajes adecuado:
private void EndGame()
{
bool isNewHighscore = false;
if(currentScore > 0)
{
int lowestHighscore = (this.HighscoreList.Count > 0 ? this.HighscoreList.Min(x => x.Score) : 0);
if((currentScore > lowestHighscore) || (this.HighscoreList.Count < MaxHighscoreListEntryCount))
{
bdrNewHighscore.Visibility = Visibility.Visible;
txtPlayerName.Focus();
isNewHighscore = true;
}
}
if(!isNewHighscore)
{
tbFinalScore.Text = currentScore.ToString();
bdrEndOfGame.Visibility = Visibility.Visible;
}
gameTickTimer.IsEnabled = false;
}
El método básicamente verifica si todavía hay puntos disponibles en la lista de puntuación alta (definimos un máximo de 5 entradas) o si el usuario acaba de superar una de las puntuaciones existentes; de ser así, permitimos que el usuario agregue su nombre mostrando el contenedor bdrNewHighscore . Si no se logró una nueva puntuación alta, en su lugar, mostramos el contenedor bdrEndOfGame . Asegúrese de definir la constante MaxHighscoreListEntryCount :
const int MaxHighscoreListEntryCount = 5;
También he modificado el método ContentRendered() - ahora que tenemos una pantalla bonita de bienvenida, no queremos que el juego empiece automáticamente. En su lugar, instaremos al usuario para que presione Espacio para empezar el juego o clicar un botón para ver la lista de mejores puntuaciones antes de que el juego comience, asi que simplemente borramos (o comentamos) la llamada a StartNewGame():
private void Window_ContentRendered(object sender, EventArgs e)
{
DrawGameArea();
//StartNewGame();
}
Con todo eso en su lugar, comience el juego y dé lo mejor de sí mismo: tan pronto como termine el juego, ¡debería haber llegado a su nueva lista de puntuación SnakeWPF!
Resumen
En este artículo, realizamos MUCHAS mejoras a nuestra implementación SnakeWPF. La más obvia es, por supuesto, la lista de puntuaciones altas, que requirió un poco de marcado / código adicional, ¡pero valió la pena! Además de eso, hicimos algunas mejoras de usabilidad agradables, mientras que una vez más, nuestro proyecto se parecía aún más a un juego real.