This article has been localized into Chinese 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!
改善WPF贪吃蛇:加一个高分榜
在过去的文章中,我们做了一些视觉实现到我们的WPF贪吃蛇实现中。在这篇文章,我将想去添加一个非常酷的功能:一个高分榜!,除此之外,我将想去让游戏对用户更友好一点,通过添加一个欢迎界面。我也将非常没有游戏性的"You died"消息框换成一个游戏内的屏幕。
我们需要相当多的额外的扩展代码和标签给它,但是先让我们从简单的部分开始-XAML
XAML
第一件我将想要做的事就是去添加一堆XAML到Snake Window。这将会主要由4个新的容器组成(在本例子为Border 控件),其中将包括一些子控件,用来支持各种情况:
- 当游戏开始时,一个容器用来展示欢迎信息,提示关于要使用的控件等等。
- 一个容器用来展示高分榜
- 当用户击败其中一个高分时,将显示一个容器,其中包括用于输入名字的文本框
- 当用户死亡的时候,将显示一个容器,但是还没进入高分榜(代替我们之前使用的无聊的提示框)
我们将会添加这些容器到GameArea面板,然后当我们不需要它们时候简单地隐藏掉。如上所述,每个容器将服务一个不同的实现和包括相当多的标签,但是我们只用那些我们曾经在教程中讨论到的WPF控件
欢迎信息
在GameArea Canvas控件中添加这段XAML:
<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>
这简要地告诉用户所有关于游戏的一切,如何控制贪吃蛇和如何开始游戏。包含所有内容的Border控件,最初是可见的,因此它将会成为当游戏开始时遇到的第一件事。在这个屏幕底部,我添加一个用于展示高分榜的按钮(我们一会将会添加)。这个Click事件处理器之后会实现在Code-behind 中。
高分榜
现在它会更加复杂一点,因为我想要用WPF的方式做这个然后使用数据绑定去展示高分列表,而不是手动创建和更新列表。但是别担心,随着进一步发展,我会解释一切。首先,在GameArea Canvas中添加一段XAML,像我们之前做的Canvas 那样,如上所述,包含所有我们的Border控件,它们每个都提供它们自己的功能给我们的游戏:
<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>
注意这个Border 如何被初始化为不展示出来的(Visibility = Collapsed)。我们使用一个ItemsControl (我们过去在这个教程中讨论过),使用一个叫HighScoreListViewSource自定义的ItemsSource 。我们将会使用一个CollectionViewSource 来确保我们绑定到的总是正确排序的集合。我们需要去在Window XAML中定义这个资源,因此作为一个Window的子标签,你应该添加这段标记,让你的Window声明看起来这样:
<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>
................
注意我们添加了一个新的引用:The xmlns:scm,用来访问描述排序的类型。我也添加了 x:Name 属性然后设置它为window,为了我们能够在Code-behind引用声明在MainWindow 类上的成员。
在 Window.Resources中,我已经添加一个新的CollectionViewSource。它将会使用绑定去附加到一个叫HighscoreList的属性上,我们将其定义在Code-behind中。也注意我添加了一个SortDescription 进去,指定列表应该按名为Score的属性排序,基本上意味着最高分数将会展示在第一位,以此类推。
在Code-behind,我们需要去定义一个叫HighscoreList的属性,ItemsSource 依赖于它,但是我们将会在我们添加完最后的XAML后再讨论它。
新的高分
当用户打败一个存在的高分时,我们将会展示一个好看的消息框。XAML看起来像这样,然后再一次,它应该在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>
所有这些都非常简单,只需要一些文本、用于输入名字的TextBox 文本框和一个按钮去点击添加进列表,我们随后将会定义BtnAddToHighscoreList_Click事件处理器
“Oh 不-你死了!”
这最后部分就是“Oh 不,你死了,然后你不能进入高分榜”的界面,我们将用它去代替乏味的消息框,我们之前做过同样的事。XAML代码起来像这样
<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>
它通知用户不幸的事件发生,展示最后的分数,和告诉用户如何去开始一个新的游戏,非常简单!
Code-behind
有了全部的XAML,我们终于准备去实现Code-behind的东西了!首先,我们需要去实现我们定义在XAML的事件处理器们。这是“展示高分榜”按钮:
private void BtnShowHighscoreList_Click(object sender, RoutedEventArgs e)
{
bdrWelcomeMessage.Visibility = Visibility.Collapsed;
bdrHighscoreList.Visibility = Visibility.Visible;
}
非常简单,如你所见,当按钮被点击,我们隐藏欢迎的消息框,然后我们展示高分榜-我们现在就加上。
实现高分榜
我们拥有的另外一个事件处理器与向高分榜添加新的条目有关,但是为了它,我们需要一些其他条件,首先,持有高分条目的实际属性:
public ObservableCollection<SnakeHighscore> HighscoreList
{
get; set;
} = new ObservableCollection<SnakeHighscore>();
如你所见,这个一个 ObservableCollection,持有SnakeHighscore类型。首先,确定包含持有ObservableCollection类型的命名空间:
using System.Collections.ObjectModel;
然后实现SnakeHighscore类:
public class SnakeHighscore
{
public string PlayerName { get; set; }
public int Score { get; set; }
}
非常简单的一个类,如你所见,只是作为一个容器给分数和玩家姓名(谁能够添加高分进高分列表的)。
加载/保存高分榜
我也也需要一些代码去加载和保存列表。当一个新的条目添加入列表时Save方法将被调用,当我们游戏开始时Load方法被调用。我将会用一个简单的XML文件去保存列表,这将允许我们使用内置的XmlSerializer类去自动加载和保存列表。
这有许多加载/保存数据的方式,以及其他相关格式,像JSON或者甚至一个纯文本文件,但是我需要这部分代码尽可能少,因为这不和一个WPF教程相关。还有,XmlSerializer 方法使代码非常灵活,你能够轻松的添加一个新的属性到SnakeHighscore 类,然后它们将会被自动保留。这是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);
}
}
}
你需要为这去包含一些额外的命名空间:
using System.IO;
using System.Xml.Serialization;
务必在Window的构造器中调用LoadHighscoreList() 方法,例如:
public SnakeWPFSample()
{
InitializeComponent();
gameTickTimer.Tick += GameTickTimer_Tick;
LoadHighscoreList();
}
接下来,我们实现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);
}
}
当我们添加一个新的条目,Save方法是最为相关的调用,这发生在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;
}
这非常简单,我们尝试去觉得如果新的输入应该被添加在列表的顶部(一个新的最高分!)或者它是否属于列表下方。一旦我们有了新的索引,我们插入新的SnakeHighscore类的实例,使用当前分数和玩家输入的姓名。然后我们从列表的底部移除任何不需要的条目,如果列表突然有了超过我们想要的条目(MaxHighscoreListEntryCount)。然后我们保存列表(SaveHighscoreList())和隐藏bdrNewHighscore容器,将视图切换到bdrHighscoreList容器
但是这里仍然有些事要做,首先,这些新的界面(死亡消息框,高分榜等等)需要当每次一个新的游戏开始时被隐藏。因此,我们在上一篇文章实现的StartNewGame()方法的顶部现在应该看起来像这样:
private void StartNewGame()
{
bdrWelcomeMessage.Visibility = Visibility.Collapsed;
bdrHighscoreList.Visibility = Visibility.Collapsed;
bdrEndOfGame.Visibility = Visibility.Collapsed;
........
接下来我们需要做的事就是去修改EndGame()方法,不仅仅只是展示消息框,我们需要检查是否用户刚刚进入高分列表,然后展示适当的消息容器:
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;
}
这个方法检查高分列表(我们定义一个5条目的最大值)是否还有可用的位置,或者是否用户刚好击败其中一个存在的分数-如果是,我们通过显示bdrNewHighscore容器允许用户添加他们的姓名。如果没有一个新的高分被完成,而我们显示bdrEndOfGame。一定要定义MaxHighscoreListEntryCount常量:
const int MaxHighscoreListEntryCount = 5;
我修改了 ContentRendered()方法 - 讓遊戲有一個好看的歡迎畫面,因為我們不希望遊戲自動開始。相反地,我們希望在遊戲開始之前使用者按下空格鍵開始遊戲或者點擊一個按鈕查看最高分列表,因此我們只需要刪除(或注釋)對 StartNewGame() 的調用:
private void Window_ContentRendered(object sender, EventArgs e)
{
DrawGameArea();
//StartNewGame();
}
有了这些,开始游戏,然后做你最好的,一旦游戏的结束,你应该希望你能够进入一个全新的WPF贪吃蛇的高分榜
小结
在这篇文章,我们对我们的WPF贪吃蛇实现做了许多改进。最明显的一个当然是高分榜,我们需要相当多额外的标签/代码,但这完全值得!最重要的是,我们做了许多很好的可用性改进,再一次让我们的项目看起来更像一个真正的游戏。