This article has been localized into Ukrainian 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!
Покращення SnakeWPF: додавання списку найвищих балів
У попередній статті ми зробили багато візуальних покращень у нашій реалізації SnakeWPF. У цій статті я хотів би додати дуже класну функцію: Список найкращих результатів! Крім того, я хотів би зробити гру трохи зручнішою для користувача, додавши екран привітання. Я також заміню дуже неігрове вікно повідомлення "Ви померли" на екран у грі.
Нам знадобиться чимало додаткового коду та розмітки для цього, але почнемо з найпростішої частини — XAML!
XAML
Перше, що я хотів би зробити, це додати купу XAML до вікна Snake. Це в основному складатиметься з 4 нових контейнерів (у цьому випадку елементи керування Border), які розміщуватимуть низку дочірніх елементів керування для підтримки різних ситуацій:
- Один контейнер для відображення вітального повідомлення під час запуску гри, яке інформує про елементи керування тощо.
- Один контейнер для відображення списку найкращих результатів
- Один контейнер для відображення, коли користувач переміг один із найвищих балів, включаючи TextBox для введення імені
- Один контейнер для відображення, коли користувач помирає, але не потрапляє до списку найкращих результатів (замінює нудний MessageBox, який ми використовували раніше)
Ми додамо ці контейнери до полотна GameArea, а потім просто приховаємо їх, коли вони нам не знадобляться. Як згадувалося, кожен контейнер виконуватиме різну функцію та міститиме досить багато розмітки, але ми використовуємо лише елементи керування WPF, які вже обговорювалися в цьому посібнику.
Вітальне повідомлення
Додайте цей фрагмент XAML всередину елемента керування 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>
Він коротко розповідає користувачеві, про що гра, як керувати Змійкою та як розпочати гру. Рамка, яка містить весь контент, спочатку видима, тому це буде перше, що користувач побачить, коли гра запуститься. У нижній частині екрана я додав кнопку для відображення списку найкращих результатів (яку ми додамо за хвилину). Обробник події Click буде реалізовано в коді програми пізніше.
Список найвищих балів
Тепер це стає трохи складніше, оскільки я хочу зробити це у стилі WPF та використовувати прив'язку даних для відображення списку найкращих результатів, замість того, щоб, наприклад, створювати та оновлювати список вручну. Але не хвилюйтеся, я поясню все це по ходу роботи. Спочатку додайте цей фрагмент XAML всередину GameArea Canvas, як ми робили раніше - 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>
Зверніть увагу, що ця рамка спочатку не відображається (Visibility = Collapsed). Ми використовуємо ItemsControl (ми вже говорили про це раніше в цьому посібнику) з користувацьким ItemsSource під назвою HighScoreListViewSource. Ми використовуватимемо 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>
................Зверніть увагу, що я приховано додав нове посилання: xmlns:scm, яке використовується для доступу до типу SortDescription. Я також додав властивість x:Name та встановив для неї значення window, щоб ми могли посилатися на члени, визначені в класі MainWindow, у коді на основі коду.
У Window.Resources я додав новий об'єкт CollectionViewSource. Він використовує зв'язування для приєднання до властивості під назвою HighscoreList, яку ми визначимо в коді. Зверніть також увагу, що я додав до нього SortDescription, який вказує, що список має бути відсортований за спадком за властивістю під назвою Score, що по суті означає, що найвищий бал буде відображатися першим тощо.
У коді на основі коду нам потрібно визначити властивість під назвою 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>
Все дуже просто, з деяким текстом, текстовим полем для введення назви та кнопкою, на яку потрібно натиснути, щоб додати елемент до списку — ми визначимо обробник події BtnAddToHighscoreList_Click пізніше.
«О ні — ти помер!»
Остання частина — це екран «О ні, ви померли і не потрапили до списку найкращих результатів», який ми використовуватимемо для заміни нудного MessageBox, який раніше робив те саме. 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 ми нарешті готові реалізувати код на основі коду! Спочатку нам потрібно реалізувати обробники подій, які ми визначили в 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;Обов'язково викличте метод 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(). Замість того, щоб просто відображати MessageBox, нам потрібно перевірити, чи користувач щойно потрапив до списку найкращих результатів, а потім відобразити відповідний контейнер повідомлень:
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();
}З усім цим налаштованим, почніть гру та зробіть усе можливе — щойно гра закінчиться, сподіваємося, ви потрапите до свого нового списку найкращих результатів SnakeWPF!
Короткий зміст
У цій статті ми внесли БАГАТО покращень до нашої реалізації SnakeWPF. Найбільш очевидним з них, звичайно ж, є список найвищих балів, який вимагав чимало додаткової розмітки/коду, але воно того варте! Крім того, ми зробили кілька приємних покращень зручності використання, знову ж таки зробивши наш проєкт ще більше схожим на справжню гру.