This article has been localized into Vietnamese by the community.
A special thanks goes out to user #4038 for the Vietnamese translation of this article: Trung Soo
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!
Cải thiện SnakeWPF: Thêm danh sách high score
Trong bài viết trước, chúng tôi đã thực hiện rất nhiều cải tiến trực quan cho việc triển khai SnakeWPF của chúng tôi. Trong bài viết này, tôi muốn thêm một tính năng rất hay: Một danh sách điểm cao! Trên hết, tôi muốn làm cho trò chơi thân thiện hơn một chút, bằng cách thêm một màn hình chào mừng. Tôi cũng sẽ thay thế hộp thư "You died" bằng hộp thông báo với một màn hình trong trò chơi.
Chúng tôi cần khá nhiều code bổ sung cho việc này, nhưng hãy bắt đầu với phần dễ dàng - XAML!
XAML
Điều đầu tiên tôi muốn làm là thêm một loạt XAML vào cửa sổ Snake. Điều này chủ yếu sẽ bao gồm 4 container mới (trong trường hợp này là Border controls), sẽ lưu trữ một loạt các điều khiển con để hỗ trợ các tình huống khác nhau:
- Một container để hiển thị thông báo chào mừng khi trò chơi bắt đầu, thông báo về các điều khiển sẽ được sử dụng, v.v.
- Một container để hiển thị danh sách điểm cao
- Một container để hiển thị khi người dùng đã đánh bại một trong những điểm số cao, bao gồm cả TextBox để nhập tên
- Một container để hiển thị khi người dùng chết, nhưng chưa đưa nó vào danh sách điểm cao (thay thế MessageBox nhàm chán mà chúng tôi đã sử dụng trước đó)
Chúng tôi sẽ thêm các containers này vào Canvas GameArea và sau đó chỉ cần ẩn chúng khi chúng tôi không cần chúng. Như đã đề cập, mỗi container sẽ phục vụ một mục đích khác nhau và bao gồm khá nhiều đánh dấu(markup), nhưng chúng tôi chỉ sử dụng các điều khiển WPF đã được thảo luận trong hướng dẫn này.
Thông điệp chào mừng
Thêm đoạn XAML này vào trong điều khiển GameArea Canvas:
<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>
Nó nói ngắn gọn cho người dùng biết tất cả về trò chơi, cách điều khiển con rắn và cách bắt đầu trò chơi. Border, chứa tất cả nội dung, ban đầu có thể nhìn thấy, vì vậy đây sẽ là điều đầu tiên người dùng gặp khi trò chơi bắt đầu. Ở phía dưới màn hình, tôi đã thêm Nút để hiển thị danh sách điểm cao (chúng tôi sẽ thêm sau đây). Trình xử lý sự kiện Click sẽ được triển khai trong Code-behind.
Danh sách điểm cao (High score list)
Bây giờ nó phức tạp hơn một chút, vì tôi muốn thực hiện theo cách WPF và sử dụng liên kết dữ liệu để hiển thị danh sách điểm cao thay vì xây dựng và cập nhật danh sách theo cách thủ công. Nhưng đừng lo lắng, tôi sẽ giải thích tất cả. Đầu tiên, hãy thêm đoạn XAML này vào Canvas GameArea, giống như chúng ta đã làm trước đây - Canvas sẽ giữ tất cả các điều khiển Border của chúng, mỗi cái đều cung cấp một phần chức năng riêng cho trò chơi của chúng tôi:
<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>
Lưu ý cách viền này ban đầu không được hiển thị (Visibility = Collapsed). Chúng tôi sử dụng một ItemControl (chúng tôi đã nói về điều này trước đây trong hướng dẫn này), với một ItemSource tùy chỉnh được gọi là HighScoreListViewSource. Chúng tôi sẽ sử dụng CollectionViewSource để đảm bảo rằng bộ sưu tập chúng tôi liên kết luôn được sắp xếp hợp lý. Chúng ta cần xác định tài nguyên này trong Window XAML, vì vậy khi nhìn vào thẻ Window, bạn nên thêm phần đánh dấu này, làm cho khai báo Window của bạn trông như thế này:
<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>
................
Lưu ý rằng tôi đã lẻn thêm vào một tham chiếu mới: xmlns:scm, được sử dụng để truy cập loại SortDescription. Tôi cũng đã thêm thuộc tính x:Name và đặt nó vào window, để chúng ta có thể tham chiếu các thành viên được xác định trên lớp MainWindow ở Code-behind.
Trong Window.Resource, tôi đã thêm CollectionViewSource mới. Nó sử dụng ràng buộc để đính kèm vào một thuộc tính có tên HighscoreList, mà chúng ta sẽ định nghĩa trong Code-behind. Cũng lưu ý rằng tôi thêm một SortDescription vào nó, chỉ định rằng danh sách nên được sắp xếp DES(descendent ) bởi một thuộc tính có tên là Điểm, về cơ bản có nghĩa là điểm cao nhất sẽ được hiển thị trước tiên và cứ thế.
Trong Code-behind, chúng ta cần xác định thuộc tính được gọi là HighscoreList, mà ItemSource dựa vào, nhưng chúng ta sẽ nhận được điều đó sau khi chúng ta hoàn thành việc thêm XAML cuối cùng.
Kỉ lục mới
Khi người dùng đánh bại một số điểm cao hiện có, chúng tôi sẽ hiển thị một thông báo độc đáo về nó. XAML trông như thế này và một lần nữa, nó nên được thêm vào trong 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>
Tất cả đều rất đơn giản với một số văn bản, TextBox để nhập tên và Nút để nhấp để thêm vào danh sách - chúng tôi sẽ xác định trình xử lý sự kiện BtnAddToHighscoreList_Click sau.
"Oh no - you died!"
Phần cuối cùng là màn hình "Oh no - you died" và bạn không đưa nó vào danh sách điểm số cao, chúng tôi sẽ sử dụng để thay thế MessageBox nhàm chán đã làm điều tương tự trước đây. XAML trông như thế này:
<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>
Nó thông báo cho người dùng về lượt không may xảy ra, hiển thị điểm số cuối cùng và cho người dùng biết cách bắt đầu một trò chơi mới - khá đơn giản!
Code-behind
Với tất cả XAML đã có, cuối cùng chúng tôi cũng sẵn sàng triển khai công cụ Code-behind! Trước tiên, chúng ta cần triển khai các trình xử lý sự kiện mà chúng ta đã xác định trong XAML. Đây là một trong những "Show high score list" button:
private void BtnShowHighscoreList_Click(object sender, RoutedEventArgs e)
{
bdrWelcomeMessage.Visibility = Visibility.Collapsed;
bdrHighscoreList.Visibility = Visibility.Visible;
}
Khá đơn giản, như bạn có thể thấy - khi nhấp vào nút, chúng tôi sẽ ẩn thông báo chào mừng và sau đó chúng tôi sẽ hiển thị danh sách điểm cao - chúng tôi sẽ thêm điều đó ngay bây giờ.
Thực hiện danh sách điểm cao
Trình xử lý sự kiện khác mà chúng tôi có liên quan đến việc thêm một mục mới vào danh sách điểm cao, nhưng để làm điều đó, chúng tôi cần một vài bổ sung khác - trước hết, tài sản thực tế để giữ các mục điểm cao:
public ObservableCollection<SnakeHighscore> HighscoreList
{
get; set;
} = new ObservableCollection<SnakeHighscore>();
Như bạn có thể thấy, đây là một ObservableCollection, giữ kiểu SnakeHighscore. Trước tiên, hãy chắc chắn namespace bao gồm kiểu ObservableCollection:
using System.Collections.ObjectModel;
Sau đó implement lớp SnakeHighscore:
public class SnakeHighscore
{
public string PlayerName { get; set; }
public int Score { get; set; }
}
Khá là một lớp đơn giản, như bạn có thể thấy - nó chỉ đóng vai trò là nơi chứa tên và điểm của người chơi đã đưa nó vào danh sách điểm cao.
Đang tải/lưu danh sách điểm cao
Chúng tôi cũng cần một số mã để tải và lưu danh sách - phương thức Save sẽ được gọi khi một mục nhập mới được thêm vào danh sách, trong khi phương thức Load được gọi khi trò chơi của chúng tôi bắt đầu. Tôi sẽ sử dụng một tệp XML đơn giản để giữ danh sách, điều này sẽ cho phép chúng tôi sử dụng lớp XmlSerializer tích hợp để tự động tải và lưu danh sách.
Có NHIỀU cách tải/lưu dữ liệu và một số định dạng có liên quan khác như JSON hoặc thậm chí là tệp văn bản thuần túy, nhưng tôi muốn giữ phần này trong càng ít dòng mã càng tốt, vì nó không phù hợp với hướng dẫn WPF. Ngoài ra, cách tiếp cận XmlSerializer làm cho mã khá linh hoạt - bạn có thể dễ dàng thêm các thuộc tính mới vào lớp SnakeHighscore và chúng sẽ được tự động duy trì. Đây là phương thức 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);
}
}
}
Bạn cần bao gồm một vài namespaces bổ sung cho điều này:
using System.IO;
using System.Xml.Serialization;
Hãy chắc chắn gọi phương thức LoadHighscoreList(), ví dụ: trong hàm tạo của Window:
public SnakeWPFSample()
{
InitializeComponent();
gameTickTimer.Tick += GameTickTimer_Tick;
LoadHighscoreList();
}
Tiếp theo, chúng tôi thực hiện phương thức 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);
}
}
Phương thức Save có liên quan nhất để gọi khi chúng ta thêm một mục mới - điều này xảy ra trong trình xử lý sự kiện BtnAddToHighscoreList_Click(), trông giống như sau:
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;
}
Điều này khá đơn giản: Chúng tôi cố gắng quyết định xem mục mới sẽ được thêm vào đầu danh sách (điểm mới tốt nhất!) Hoặc nếu nó thuộc về danh sách tiếp theo. Khi chúng tôi có chỉ mục mới, chúng tôi chèn một thể hiện mới của lớp SnakeHighscore, sử dụng điểm số hiện tại và tên được người chơi nhập vào. Sau đó, chúng tôi sẽ xóa mọi mục không mong muốn khỏi cuối danh sách, nếu danh sách đột nhiên có nhiều mục hơn chúng tôi muốn (MaxHighscoreListEntryCount). Sau đó, chúng tôi lưu danh sách ( SaveHighscoreList()) và ẩn bdrNewHighscore, chuyển chế độ xem sang bdrHighscoreList.
Nhưng vẫn còn một vài điều phải làm. Trước hết, những màn hình mới này (tin nhắn chết, danh sách điểm cao, v.v.) cần được ẩn đi mỗi khi một trò chơi mới được bắt đầu. Vì vậy, phần đầu của phương thức StartNewGame(), mà chúng ta đã triển khai trong một bài viết trước, bây giờ sẽ trông như thế này:
private void StartNewGame()
{
bdrWelcomeMessage.Visibility = Visibility.Collapsed;
bdrHighscoreList.Visibility = Visibility.Collapsed;
bdrEndOfGame.Visibility = Visibility.Collapsed;
........
Điều tiếp theo chúng ta cần làm là sửa đổi phương thức EndGame(). Thay vì chỉ hiển thị MessageBox, chúng tôi cần kiểm tra xem người dùng có đưa nó vào danh sách điểm cao hay không và sau đó hiển thị message container thích hợp:
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;
}
Về cơ bản, phương thức này kiểm tra liệu có còn chỗ trống trong bảng xếp hạng không (chúng ta cho phép tối đa 5 điểm được hiển thị) hay nếu người dùng mới đánh bại được một trong số các điểm có sẵn - nếu vậy, chúng ta cho phép người dùng thêm tên của họ bằng cách hiển thị container bdrNewHighscore. Nếu không có điểm cao mới được xác lập, ta hiển thị container bdrEndOfGame. Hãy nhớ định nghĩa biến hằng MaxHighscoreListEntryCount.
const int MaxHighscoreListEntryCount = 5;
Tôi cũng đã sửa phương thức ContentRendered() - và bây giờ khi ta đã có một màn hình chào mừng đẹp, chúng ta không muốn trò chơi của mình cứ tự động bật lên. Thay vào đó, chúng ta muốn người dùng phải bấm phím Space để bắt đầu trò chơi hoặc nhấp vào một cái nút để thấy bảng xếp hạng trước khi trò chơi bắt đầu, vì vậy ta chỉ cần bỏ (hoặc đưa vào comment) dòng gọi phương thức StartNewGame():
private void Window_ContentRendered(object sender, EventArgs e)
{
DrawGameArea();
//StartNewGame();
}
Với tất cả những gì đã có, hãy bắt đầu trò chơi và cố gắng hết sức - ngay khi trò chơi kết thúc, bạn hy vọng có thể đưa nó vào danh sách điểm số cao mới của SnakeWPF!
Tổng kết
Trong bài viết này, chúng tôi đã thực hiện rất nhiều cải tiến để triển khai SnakeWPF. Tất nhiên rõ ràng nhất là danh sách điểm cao, đòi hỏi khá nhiều đánh dấu(markup)/code bổ sung, nhưng nó hoàn toàn xứng đáng! Trên hết, chúng tôi đã thực hiện một số cải tiến khả năng sử dụng tốt, trong khi một lần nữa làm cho dự án của chúng tôi trông giống như một trò chơi thực sự.