This article has been localized into German 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!
Verbesserung von SnakeWPF: Hinzufügen einer Highscore-Liste
Im vorherigen Artikel haben wir viele visuelle Verbesserungen an unserer SnakeWPF-Implementierung vorgenommen. In diesem Artikel möchte ich ein sehr cooles Feature hinzufügen: Eine Highscore-Liste! Darüber hinaus möchte ich das Spiel benutzerfreundlicher gestalten, indem ich einen Begrüßungsbildschirm hinzufüge. Ich werde auch die sehr nicht spielerische "Du bist gestorben" -Messagebox durch einen In-Game-Bildschirm ersetzen.
Wir brauchen dafür einiges an zusätzlichem Code und Markup, aber fangen wir mit dem einfachen Teil an - der XAML!
XAML
Als Erstes möchte ich dem Snake-Fenster eine Reihe von XAML hinzufügen. Dies wird hauptsächlich aus 4 neuen Containern bestehen (in diesem Fall Grenzkontrollen), die eine Reihe von untergeordneten Kontrollen enthalten, um verschiedene Situationen zu unterstützen:
- Ein Container zum Anzeigen einer Begrüßungsnachricht zu Beginn des Spiels, zum Informieren über die zu verwendenden Steuerelemente usw.
- Ein Container zur Anzeige der Highscore-Liste
- Ein Container, der angezeigt wird, wenn der Benutzer einen der Highscores erreicht hat, einschließlich eines Textfelds zur Eingabe eines Namens
- Ein Container, der angezeigt wird, wenn der Benutzer stirbt, es aber nicht in die Highscore-Liste geschafft hat (anstelle der langweiligen MessageBox, die wir zuvor verwendet haben)
Wir werden diese Container zur GameArea Canvas hinzufügen und sie dann einfach ausblenden, wenn wir sie nicht benötigen. Wie bereits erwähnt, hat jeder Container einen anderen Zweck und enthält einige Markups. Wir verwenden jedoch nur WPF-Steuerelemente, die bereits in diesem Lernprogramm behandelt wurden.
Die Wilkommensnachricht
Fügen Sie dieses Stück XAML in das GameArea Canvas-Steuerelement ein:
<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>
Es erklärt dem Benutzer kurz, worum es im Spiel geht, wie die Schlange gesteuert wird und wie das Spiel gestartet wird. Der Rand, der den gesamten Inhalt enthält, ist zunächst sichtbar. Dies ist also das erste, was der Benutzer trifft, wenn das Spiel startet. Am unteren Rand des Bildschirms habe ich eine Schaltfläche zum Anzeigen der Highscore-Liste hinzugefügt (die wir in nur einer Minute hinzufügen werden). Der Click-Ereignishandler wird später im Code-Behind implementiert.
High Score Liste
Jetzt wird es etwas komplexer, weil ich dies auf WPF-Weise tun und die Datenbindung verwenden möchte, um die Highscore-Liste anzuzeigen, anstatt z.B.: Manuelles Erstellen und Aktualisieren der Liste. Aber keine Sorge, ich werde alles erklären, während wir weiterziehen. Fügen Sie zunächst dieses XAML-Element wie zuvor im GameArea-Canvas-Bereich hinzu. Wie bereits erwähnt, enthält der Canvas-Bereich alle unsere Border-Steuerelemente, von denen jedes seine eigenen Funktionen für unser Spiel bietet:
<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>
Beachten Sie, wie dieser Rand anfangs nicht angezeigt wird (Sichtbarkeit = Reduziert). Wir verwenden ein ItemsControl (wir haben bereits in diesem Tutorial darüber gesprochen) mit einer benutzerdefinierten ItemsSource namens HighScoreListViewSource. Wir werden eine CollectionViewSource verwenden, um sicherzustellen, dass die Sammlung, an die wir binden, immer richtig sortiert ist. Wir müssen diese Ressource in der Window-XAML definieren. Als untergeordnetes Element zum Window-Tag sollten Sie dieses Markup hinzufügen, damit Ihre Window-Deklaration wie folgt aussieht:
<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>
................
Beachten Sie, dass ich eine neue Referenz eingefügt habe: Die xmlns:scm, mit der auf den SortDescription-Typ zugegriffen wird. Ich habe auch die x:Name-Eigenschaft hinzugefügt und sie auf window gesetzt, damit wir auf Member verweisen können, die in der MainWindow-Klasse in Code-behind definiert wurden.
In den Window.Resources habe ich eine neue CollectionViewSource hinzugefügt. Die Bindung wird zum Anhängen an eine Eigenschaft namens HighscoreList verwendet, die wir im Code-behind definieren werden. Beachten Sie auch, dass ich eine SortDescription hinzufüge, die angibt, dass die Liste nach einer Eigenschaft mit dem Namen Score absteigend sortiert werden soll, was im Grunde bedeutet, dass die höchste Punktzahl zuerst angezeigt wird und so weiter.
Im Code-behind müssen wir die Eigenschaft HighscoreList definieren, auf die sich die ItemsSource stützt. Nachdem wir die letzte XAML hinzugefügt haben, werden wir darauf zurückgreifen.
Neue Bestenliste (High Score)
Wenn der Benutzer einen vorhandenen Highscore übertrifft, wird eine ansprechende Meldung angezeigt. Die XAML sieht so aus und sollte noch einmal im GameArea Canvas hinzugefügt werden:
<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>
Ganz einfach, mit etwas Text, einer Textbox zur Eingabe des Namens und einem Button zum Hinzufügen zur Liste - wir definieren später die Ereignisbehandlungsroutine BtnAddToHighscoreList_Click.
"Oh nein - du bist gestorben!"
Der letzte Teil ist der Bildschirm "Oh nein, du bist gestorben und hast es nicht in die Highscore-Liste geschafft", mit dem wir die langweilige MessageBox ersetzen, die zuvor das Gleiche getan hat. Die XAML sieht folgendermaßen aus:
<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>
Es informiert den Benutzer über die unglückliche Wende der Ereignisse, zeigt das Endergebnis an und teilt dem Benutzer mit, wie ein neues Spiel zu starten ist - ganz einfach!
Code-behind (Hintergrundcode)
Mit all der XAML sind wir endlich bereit, das Code-Behind-Zeug zu implementieren! Zuerst müssen wir die Event-Handler implementieren, die wir in der XAML definiert haben. Hier ist die für die Schaltfläche "Highscore-Liste anzeigen":
private void BtnShowHighscoreList_Click(object sender, RoutedEventArgs e)
{
bdrWelcomeMessage.Visibility = Visibility.Collapsed;
bdrHighscoreList.Visibility = Visibility.Visible;
}
Wie Sie sehen, ist es ganz einfach: Wenn Sie auf die Schaltfläche klicken, wird die Begrüßungsnachricht ausgeblendet und anschließend die Highscore-Liste angezeigt. Dies wird nun hinzugefügt.
Implementierung der Highscore-Liste
Die andere Ereignisbehandlungsroutine, die wir haben, bezieht sich auf das Hinzufügen eines neuen Eintrags zur Highscore-Liste. Dazu benötigen wir jedoch einige weitere Ergänzungen - vor allem die eigentliche Eigenschaft zum Halten der Highscore-Einträge:
public ObservableCollection<SnakeHighscore> HighscoreList
{
get; set;
} = new ObservableCollection<SnakeHighscore>();
Wie Sie sehen, ist dies eine ObservableCollection, die den Typ SnakeHighscore enthält. Stellen Sie zunächst sicher, dass der Namespace enthalten ist, der den ObservableCollection-Typ enthält:
using System.Collections.ObjectModel;
Implementieren Sie dann die SnakeHighscore -Klasse:
public class SnakeHighscore
{
public string PlayerName { get; set; }
public int Score { get; set; }
}
Wie Sie sehen, handelt es sich um eine recht einfache Klasse, die nur als Container für den Namen und die Punktzahl des Spielers dient, der es in die Highscore-Liste geschafft hat.
Laden/Speichern der Highscore-Liste
Wir benötigen auch Code zum Laden und Speichern der Liste - die Methode Save wird aufgerufen, wenn ein neuer Eintrag zur Liste hinzugefügt wird, während die Methode Load aufgerufen wird, wenn unsere Spiel beginnt Methode aufgerufen wird. Ich verwende eine einfache XML-Datei, um die Liste zu speichern. Dadurch können wir die integrierte XmlSerializer -Klasse verwenden, um die Liste automatisch zu laden und zu speichern.
Es gibt VIELE Möglichkeiten zum Laden/Speichern von Daten und einige andere relevante Formate wie JSON oder sogar eine reine Textdatei, aber ich wollte diesen Teil in so wenigen Codezeilen wie möglich belassen, da er für ein WPF-Tutorial nicht so relevant ist. Der XmlSerializer-Ansatz macht den Code außerdem ziemlich flexibel. Sie können der SnakeHighscore-Klasse problemlos neue Eigenschaften hinzufügen, die automatisch beibehalten werden. Hier ist die LoadHighscoreList() -Methode:
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);
}
}
}
Dazu müssen Sie einige zusätzliche Namespaces hinzufügen:
using System.IO;
using System.Xml.Serialization;
Stellen Sie sicher, dass Sie die LoadHighscoreList() -Methode aufrufen, z.B.: im Konstruktor des Fensters:
public SnakeWPFSample()
{
InitializeComponent();
gameTickTimer.Tick += GameTickTimer_Tick;
LoadHighscoreList();
}
Als Nächstes implementieren wir die Methode 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);
}
}
Die Save-Methode ist am relevantesten für den Aufruf, wenn wir einen neuen Eintrag hinzufügen. Dies geschieht im BtnAddToHighscoreList_Click() -Ereignishandler, der folgendermaßen aussehen sollte:
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;
}
Ganz einfach: Wir versuchen zu entscheiden, ob der neue Eintrag ganz oben in der Liste steht (ein neuer Bester!) Oder ob er weiter unten in die Liste gehört. Sobald wir den neuen Index haben, fügen wir eine neue Instanz der SnakeHighscore -Klasse unter Verwendung der aktuellen Punktzahl und des vom Spieler eingegebenen Namens ein. Wir entfernen dann alle unerwünschten Einträge vom Ende der Liste, wenn die Liste plötzlich mehr Elemente enthält, als wir möchten (MaxHighscoreListEntryCount). Dann speichern wir die Liste (SaveHighscoreList()) und verbergen den Container bdrNewHighscore, wobei die Ansicht auf den Container bdrHighscoreList wechselt.
Aber es gibt noch ein paar Dinge zu tun. Zunächst müssen diese neuen Bildschirme ("tot" Nachricht, Highscore-Liste usw.) jedes Mal ausgeblendet werden, wenn ein neues Spiel angestarrt wird. Das obere Ende der StartNewGame() -Methode, die wir in einem vorherigen Artikel implementiert haben, sollte nun folgendermaßen aussehen:
private void StartNewGame()
{
bdrWelcomeMessage.Visibility = Visibility.Collapsed;
bdrHighscoreList.Visibility = Visibility.Collapsed;
bdrEndOfGame.Visibility = Visibility.Collapsed;
........
Als Nächstes müssen wir die EndGame() -Methode ändern. Anstatt nur die MessageBox anzuzeigen, müssen wir prüfen, ob der Benutzer es gerade in die Highscore-Liste geschafft hat oder nicht, und dann den richtigen Nachrichtencontainer anzeigen:
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;
}
Die Methode prüft grundsätzlich, ob in der Highscore-Liste noch Plätze verfügbar sind (wir definieren maximal 5 Einträge) oder ob der Benutzer nur einen der vorhandenen Punkte übertrifft. In diesem Fall können Sie dem Benutzer erlauben, seinen Namen hinzuzufügen, indem Sie den bdrNewHighscore -Container anzeigen. Wurde kein neuer Highscore erzielt, wird stattdessen der Container bdrEndOfGame angezeigt. Stellen Sie sicher, dass Sie die Konstante MaxHighscoreListEntryCount definieren:
const int MaxHighscoreListEntryCount = 5;
Ich habe auch die ContentRendered() Methode geändert. Da wir jetzt einen gut aussehenden Willkommens-Bildschirm haben, möchten wir nicht, dass das Spiel automatisch startet. Wir wollen daher den Nutzer dazu bringen, die Leertaste zu drücken oder einen Knopf drückt, um die Highscore-Liste anzuzeigen, bevor das Spiel beginnt, also entfernen (oder auch kommentieren) wir den Call zu StartNewGame():
private void Window_ContentRendered(object sender, EventArgs e)
{
DrawGameArea();
//StartNewGame();
}
Starten Sie das Spiel und geben Sie Ihr Bestes - sobald das Spiel zu Ende ist, sollten Sie es hoffentlich in Ihre brandneue SnakeWPF-Highscore-Liste geschafft haben!
Zusammenfassung
In diesem Artikel haben wir viele Verbesserungen an unserer SnakeWPF-Implementierung vorgenommen. Das offensichtlichste ist natürlich die Highscore-Liste, die einiges an zusätzlichem Markup/Code erforderte, aber es hat sich absolut gelohnt! Darüber hinaus haben wir einige Verbesserungen an der Benutzerfreundlichkeit vorgenommen und unser Projekt noch einmal wie ein echtes Spiel aussehen lassen.