This article has been localized into Italian by the community.
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!
Miglioramento di SnakeWPF: aggiungere l'elenco dei migliori punteggi
Nell'articolo precedente, abbiamo apportato molti miglioramenti visivi alla nostra implementazione di SnakeWPF. In questo articolo, vorrei aggiungere una funzionalità molto interessante: Un elenco di punteggi elevati! Inoltre, vorrei rendere il gioco un po 'più user-friendly, aggiungendo una schermata di benvenuto . Sostituirò anche la casella di messaggio "You died" non molto gamey con una schermata di gioco.
Per questo abbiamo bisogno di un po 'di codice extra e di markup, ma iniziamo con la parte più semplice: XAML!
XAML
La prima cosa che vorrei fare è aggiungere un po' di XAML alla finestra di Snake. Ciò consisterà principalmente in 4 nuovi contenitori (in questo caso controlli alle frontiere), che ospiteranno una serie di controlli figlio per supportare varie situazioni:
- Un contenitore per la visualizzazione di un messaggio di benvenuto all'avvio del gioco, per informazioni sui controlli da utilizzare, ecc.
- Un contenitore per la visualizzazione dell'elenco dei punteggi migliori
- Un contenitore da visualizzare quando l'utente ha battuto uno dei punteggi migliori, incluso un TextBox per l'inserimento di un nome
- Un contenitore da visualizzare quando l'utente muore, ma non è entrato nell'elenco dei punteggi migliori (sostituendo il noioso MessageBox che abbiamo usato in precedenza)
Aggiungeremo questi contenitori nella GameArea e poi li nasconderemo semplicemente quando non ne abbiamo bisogno. Come accennato, ogni contenitore avrà uno scopo diverso e includerà un bel po 'di markup, ma usiamo solo controlli WPF che sono già stati discussi in questo tutorial.
Messaggio di benvenuto
Aggiungi questo pezzo di XAML all'interno del controllo Canvas di GameArea:
<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>
Descrive brevemente all'utente in cosa consiste il gioco, come viene controllato Snake e come iniziare il gioco. Il bordo, che contiene tutto il contenuto, è inizialmente visibile, quindi questa sarà la prima cosa che l'utente incontra all'avvio del gioco. Nella parte inferiore dello schermo, ho aggiunto un pulsante per visualizzare l'elenco dei punteggi migliori (che aggiungeremo tra un minuto). Il gestore di eventi Click verrà implementato nel Codice dietro in seguito.
Lista dei migliori punteggi
Ora diventa un po 'più complesso, perché voglio farlo nel modo WPF e utilizzare l'associazione dei dati per visualizzare l'elenco dei punteggi alti anziché ad es. compilare e aggiornare manualmente l'elenco. Ma non preoccuparti, ti spiego tutto mentre procediamo. Per prima cosa, aggiungi questo pezzo di XAML all'interno del GameArea Canvas, proprio come abbiamo fatto prima: il Canvas, come accennato, deterrà tutti i nostri controlli Border, ognuno dei quali offre il proprio pezzo di funzionalità per il nostro gioco:
<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>
Notare come questo bordo inizialmente non viene visualizzato (Visibilità = Collapsed). Usiamo un ItemsControl (ne abbiamo parlato in precedenza in questa esercitazione), con un ItemsSource personalizzato chiamato HighScoreListViewSource . Utilizzeremo CollectionViewSource per assicurarci che la raccolta a cui ci leghiamo sia sempre ordinata correttamente. Dobbiamo definire questa risorsa in Window XAML, quindi come elemento secondario al tag Window, dovresti aggiungere questo pezzo di markup, facendo apparire la tua dichiarazione Window come segue:
<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>
................
Si noti che ho introdotto un nuovo riferimento: xmlns:scm , utilizzato per accedere al tipo SortDescription. Ho anche aggiunto la proprietà x: Name e l'ho impostata su window, in modo che possiamo fare riferimento ai membri definiti nella classe MainWindow in Code-behind.
In Window.Resources, ho aggiunto un nuovo CollectionViewSource . Usa l'associazione per collegarsi a una proprietà chiamata HighscoreList, che definiremo in Code-behind. Si noti inoltre che aggiungo un SortDescription, specificando che l'elenco deve essere ordinato in base a una proprietà chiamata Score, in pratica significa che il punteggio più alto verrà visualizzato per primo e così via.
In Code-behind, dobbiamo definire la proprietà chiamata HighscoreList , su cui si basa ItemsSource, ma ci arriveremo dopo aver finito di aggiungere l'ultimo XAML.
Nuovo punteggio alto
Quando l'utente batte un punteggio elevato esistente, ci mostrerà un bel messaggio. Il XAML assomiglia a questo e, ancora una volta, va aggiunto all'interno del GameArea Canvas :
<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>
Tutto molto semplice con un po' di testo, una TextBox per l'immissione del nome e un pulsante per fare clic per aggiungere all'elenco: definiremo il gestore di eventi BtnAddToHighscoreList_Click in un secondo momento.
"Oh no: sei morto!"
L'ultima parte è la schermata "Oh no, sei morto e non sei entrato nella lista dei punteggi migliori", che useremo per sostituire il noioso MessageBox che ha fatto la stessa cosa in precedenza. XAML è simile al seguente:
<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 l'utente della sfortunata svolta degli eventi, mostra il punteggio finale e dice all'utente come iniziare una nuova partita - piuttosto semplice!
Code-behind
Con tutte le XAML a posto, siamo finalmente pronti per implementare le cose Code-behind! Innanzitutto, dobbiamo implementare i gestori di eventi che abbiamo definito in XAML. Ecco quello per il pulsante "Mostra elenco dei punteggi migliori":
private void BtnShowHighscoreList_Click(object sender, RoutedEventArgs e)
{
bdrWelcomeMessage.Visibility = Visibility.Collapsed;
bdrHighscoreList.Visibility = Visibility.Visible;
}
Abbastanza semplice, come puoi vedere - quando si fa clic sul pulsante, nascondiamo il messaggio di benvenuto e quindi visualizziamo l'elenco dei punteggi migliori - lo aggiungeremo ora.
Implementazione dell'elenco dei punteggi migliori
L'altro gestore di eventi che abbiamo riguarda l'aggiunta di una nuova voce all'elenco dei punteggi migliori, ma per questo abbiamo bisogno di un paio di altre aggiunte: prima di tutto, la proprietà effettiva per contenere le voci dei punteggi migliori:
public ObservableCollection<SnakeHighscore> HighscoreList
{
get; set;
} = new ObservableCollection<SnakeHighscore>();
Come puoi vedere, si tratta di un ObservableCollection , contenente il tipo SnakeHighscore . Innanzitutto, assicurati di includere il tipo ObservableCollection:
using System.Collections.ObjectModel;
Poi implementa la classe SnakeHighscore:
public class SnakeHighscore
{
public string PlayerName { get; set; }
public int Score { get; set; }
}
Una classe semplice, come puoi vedere - serve solo come contenitore per il nome e il punteggio del giocatore che è entrato nella lista dei punteggi migliori.
Caricamento / salvataggio dell'elenco dei punteggi migliori
Abbiamo anche bisogno di un po' di codice per caricare e salvare l'elenco: il metodo Save verrà chiamato quando una nuova voce viene aggiunta all'elenco, mentre il metodo Load viene chiamato quando il nostro il gioco inizia. Userò un semplice file XML per contenere l'elenco; la classe XmlSerializer integrata ci consentità di caricare e salvare automaticamente l'elenco.
Esistono molti modi per caricare / salvare i dati e molti altri formati pertinenti come JSON o anche un semplice file di testo, ma volevo mantenere questa parte nel minor numero di righe di codice possibile, poiché non è così rilevante per un'esercitazione WPF. Inoltre, l'approccio XmlSerializer rende il codice piuttosto flessibile: puoi facilmente aggiungere nuove proprietà alla classe SnakeHighscore e queste verranno automaticamente mantenute. Ecco il metodo 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);
}
}
}
Si devono includere una paio di namespaces:
using System.IO;
using System.Xml.Serialization;
Assicurati di chiamare il metodo LoadHighscoreList (), ad es. nel costruttore della Window:
public SnakeWPFSample()
{
InitializeComponent();
gameTickTimer.Tick += GameTickTimer_Tick;
LoadHighscoreList();
}
Successivamente, implementiamo il metodo 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);
}
}
Il metodo Save viene chiamato quando aggiungiamo una nuova voce: ciò si verifica nel gestore dell'evento BtnAddToHighscoreList_Click () :
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;
}
È abbastanza semplice: proviamo a decidere se aggiungere la nuova voce in cima all'elenco (un nuovo migliore!) o se farlo apparire più in basso nell'elenco. Una volta che abbiamo il nuovo indice, inseriamo una nuova istanza della classe SnakeHighscore , usando il punteggio attuale e il nome inserito dal giocatore. Rimuoviamo quindi tutte le voci indesiderate dalla parte inferiore dell'elenco, se l'elenco contiene improvvisamente più elementi di quelli desiderati (MaxHighscoreListEntryCount). Quindi salviamo l'elenco ( SaveHighscoreList () ) e nascondiamo il contenitore bdrNewHighscore , passando la vista al contenitore bdrHighscoreList .
Ma ci sono ancora un paio di cose da fare. Prima di tutto, queste nuove schermate (messaggio morto, elenco dei punteggi migliori, ecc.) devono essere nascoste ogni volta che viene avviato un nuovo gioco. Quindi, la parte superiore del metodo StartNewGame (), che abbiamo implementato in un articolo precedente, dovrebbe ora apparire così:
private void StartNewGame()
{
bdrWelcomeMessage.Visibility = Visibility.Collapsed;
bdrHighscoreList.Visibility = Visibility.Collapsed;
bdrEndOfGame.Visibility = Visibility.Collapsed;
........
La prossima cosa che dobbiamo fare è modificare il metodo EndGame () . Invece di visualizzare il MessageBox, dobbiamo verificare se l'utente è appena entrato nell'elenco dei punteggi migliori o meno e quindi visualizzare il contenitore di messaggi appropriato:
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;
}
Il metodo controlla fondamentalmente se ci sono ancora punti disponibili nell'elenco dei punteggi migliori (definiamo un massimo di 5 voci) o se l'utente ha appena battuto uno dei punteggi esistenti - in tal caso, consentiamo all'utente di aggiungere il proprio nome visualizzando il bdrNewHighscore . Se non viene raggiunto alcun nuovo punteggio elevato, viene visualizzato il contenitore bdrEndOfGame . Assicurati di definire la costante MaxHighscoreListEntryCount :
const int MaxHighscoreListEntryCount = 5;
Ho anche modificato il metodo ContentRendered() - adesso che abbiamo una bella schermata di benvenuto, non vogliamo che il gioco inizi automaticamente. Invitiamo il giocatore a premere Spazio per iniziare il gioco oppure a cliccare un bottone per vedere la classifica prima che il gioco inizi. Quindi rimuoviamo (o commentiamo) la chiamata a StartNewGame():
private void Window_ContentRendered(object sender, EventArgs e)
{
DrawGameArea();
//StartNewGame();
}
Ciò fatto, inizia il gioco e fai del tuo meglio - non appena il gioco termina, dovresti potere inserire nella tua nuova classifica dei punteggi migliori di SnakeWPF!
Riepilogo
In questo articolo, abbiamo apportato MOLTI miglioramenti alla nostra implementazione di SnakeWPF. Il più ovvio è ovviamente l'elenco dei punteggi migliori, che ha richiesto un po 'di markup / codice extra, ma ne è valsa la pena! Inoltre, abbiamo apportato alcuni miglioramenti all'usabilità, mentre ancora una volta abbiamo reso il nostro progetto ancora più simile a un gioco reale.