TOC

This article has been localized into Czech by the community.

Tvoříme počítačovou hru: SnakeWPF:
Chapter introduction:

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!

Vylepšení hry SnakeWPF: Přidání seznamu nejlepších výsledků

V předchozím článku jsme provedli mnoho vizuálních vylepšení naší implementace hry SnakeWPF. V tomto článku bych rád přidal velmi skvělou funkci: Seznam nejlepších výsledků! Kromě toho bych chtěl hru trochu uživatelsky přívětivější tím, že přidám uvítací obrazovku. Také nahradím velmi nehráčské "Zemřel jsi" - messagebox obrazovkou v rámci hry.

Potřebujeme docela dost dalšího kódu a značek pro toto, ale začněme s jednodušší částí - XAML!

XAML

První věcí, kterou bych chtěl udělat, je přidat spoustu XAML do okna hada. To se převážně skládá ze 4 nových kontejnerů (v tomto případě ovládacích prvků Border), které budou hostit škálu ovládacích prvků pro podporu různých situací:

  • Jeden kontejner pro zobrazení uvítací zprávy při spuštění hry, informující o ovládání atd.
  • Jeden kontejner pro zobrazení seznamu nejlepších výsledků.
  • Jeden kontejner pro zobrazení, když uživatel překoná jedno z nejlepších skóre, včetně TextBoxu pro zadání jména.
  • Jeden kontejner pro zobrazení, když uživatel zemře, ale nepodaří se mu dostat do seznamu nejlepších výsledků (nahrazení nudného MessageBoxu, který jsme dříve používali).

Tyto kontejnery přidáme do plátna GameArea a jednoduše je skryjeme, když je nepotřebujeme. Jak již bylo zmíněno, každý kontejner bude sloužit jinému účelu a bude zahrnovat docela dost značek, ale použijeme pouze ovládací prvky WPF, o kterých bylo v tomto tutoriálu již řečeno.

Uvítací zpráva

Přidejte tento úsek XAML do ovládacího prvku Canvas s názvem 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>

Stručně informuje uživatele, o čem hra je, jak se ovládá Had a jak začít hru. Rámeček, který obsahuje veškerý obsah, je zpočátku viditelný, takže toto bude první věc, kterou uživatel uvidí po spuštění hry. Na spodní část obrazovky jsem přidal tlačítko pro zobrazení seznamu nejlepších výsledků (který přidáme za chvíli). Ovladač události Click bude implementován později v kódu Code-behind.

Seznam nejlepších výsledků

Nyní to bude trochu složitější, protože chci udělat věci ve stylu WPF a použít data-binding k zobrazení seznamu nejlepších výsledků místo ručního vytváření a aktualizování seznamu. Ale nebojte se, vysvětlím vše, jak budeme postupovat. Nejprve přidejte tento úsek XAML uvnitř ovládacího prvku Canvas jako dříve - Canvas, jak již bylo zmíněno, bude držet všechny naše ovládací prvky Border, z nichž každý nabídne vlastní část funkcionality pro naši hru:

<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>

Všimněte si, že tento Border není zpočátku zobrazen (Visibility = Collapsed). Používáme ItemsControl (o tom jsme již dříve mluvili v tomto tutoriálu), s vlastním ItemsSource s názvem HighScoreListViewSource. Budeme používat CollectionViewSource, abychom se ujistili, že kolekce, ke které se vážeme, je vždy správně seřazena. Tuto zdrojovou položku musíme definovat v XAML okna, takže jako potomek značky Window přidejte tento úsek značek, aby vypadala deklarace okna takto:

<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>
    ................

Všimněte si, že jsem tam schválně vepsal nový odkaz: xmlns:scm, používaný k přístupu k typu SortDescription. Také jsem přidal vlastnost x:Name a nastavil ji na window, abychom mohli odkazovat na členy definované ve třídě MainWindow v kódu Code-behind.

Ve zdrojích okna jsem přidal nový CollectionViewSource. Používá vazbu k připojení k vlastnosti s názvem HighscoreList, kterou definujeme v kódu Code-behind. Všimněte si také, že jsem k němu přidal SortDescription, kde specifikujeme, že seznam by měl být seřazen sestupně podle vlastnosti s názvem Score, což znamená, že nejvyšší skóre bude zobrazeno první a tak dále.

V kódu Code-behind musíme definovat vlastnost s názvem HighscoreList, na které se spoléhá ItemsSource, ale k tomu se dostaneme až poté, co dokončíme přidání posledního XAML.

Nové nejvyšší skóre

Když uživatel překoná existující nejlepší skóre, zobrazíme o tom hezky vypadající zprávu. XAML vypadá takto a opět by měl být přidán dovnitř Canvasu s názvem GameArea:

<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>

Vše velmi jednoduché s nějakým textem, TextBoxem pro zadání jména a tlačítkem pro přidání do seznamu - ovladač události (event handler)BtnAddToHighscoreList_Click definujeme později.

"Ale ne - zemřel jsi"

Poslední částí je obrazovka "Ale ne, zemřel jsi a nedostal ses do seznamu nejlepších výsledků", kterou použijeme k nahrazení nudného MessageBoxu, který dělal totéž dříve. XAML vypadá takto:

<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>

Informuje uživatele o nešťastném průběhu událostí, zobrazuje konečné skóre a říká uživateli, jak začít novou hru - docela jednoduché!

Code-behind

Se všemi XAML kódy na svém místě jsme konečně připraveni implementovat kód pro Code-behind! Nejprve musíme implementovat ovladače událostí, které jsme definovali v XAML. Zde je jeden pro tlačítko "Zobrazit seznam nejlepších výsledků":

private void BtnShowHighscoreList_Click(object sender, RoutedEventArgs e)    
{    
    bdrWelcomeMessage.Visibility = Visibility.Collapsed;    
    bdrHighscoreList.Visibility = Visibility.Visible;    
}

Docela jednoduché, jak můžete vidět - po kliknutí na tlačítko skryjeme uvítací zprávu a pak zobrazíme seznam nejlepších výsledků - to přidáme nyní.

Implementace seznamu nejlepších výsledků

Druhý ovladač události se týká přidání nové položky do seznamu nejlepších výsledků, ale pro to potřebujeme ještě několik dalších doplňků - především samotnou vlastnost pro uchování položek s nejlepšími výsledky:

public ObservableCollection<SnakeHighscore> HighscoreList
{
    get; set;
} = new ObservableCollection<SnakeHighscore>();

Jak můžete vidět, jedná se o ObservableCollection, která obsahuje typ SnakeHighscore. Nejprve se ujistěte, že jste zahrnuli prostor názvů, který obsahuje typ ObservableCollection:

using System.Collections.ObjectModel;

Poté implementujte třídu SnakeHighscore:

public class SnakeHighscore
{
    public string PlayerName { get; set; }

    public int Score { get; set; }
}

Docela jednoduchá třída, jak můžete vidět - slouží pouze jako kontejner pro jméno a skóre hráče, který se dostal do seznamu nejlepších výsledků.

Načítání/ukládání seznamu nejlepších výsledků

Také potřebujeme nějaký kód pro načítání a ukládání seznamu - metoda Save bude volána, když je do seznamu přidána nová položka, zatímco metoda Load je volána, když se naše hra spustí. Budu používat jednoduchý XML soubor pro uchování seznamu, což nám umožní používat vestavěnou třídu XmlSerializer k automatickému načítání a ukládání seznamu.

Existuje MNOHO způsobů načítání/ukládání dat a několik dalších relevantních formátů, jako je například JSON nebo dokonce prostý textový soubor, ale chtěl jsem tuto část mít v co nejméně řádcích kódu, protože to není tak relevantní pro tutoriál WPF. Navíc přístup pomocí XmlSerializeru činí kód poměrně flexibilním - můžete snadno přidávat nové vlastnosti třídě SnakeHighscore a budou automaticky ukládány. Zde je metoda 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);
}
    }
}

Pro to je potřeba zahrnout několik dalších názvových prostorů (namespace):

using System.IO;
using System.Xml.Serialization;

Nezapomeňte zavolat metodu LoadHighscoreList(), například v konstruktoru okna:

public SnakeWPFSample()
{
    InitializeComponent();
    gameTickTimer.Tick += GameTickTimer_Tick;
    LoadHighscoreList();
}

Následně implementujeme metodu 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);
    }
}

Metoda Save je nejrelevantnější k volání, když přidáme nový záznam - to se děje v ovladači události BtnAddToHighscoreList_Click(), který by měl vypadat takto:

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;
}

Je to docela jednoduché: Pokusíme se rozhodnout, zda by nový záznam měl být přidán na začátek seznamu (nový nejlepší!) nebo zda patří dále dolů v seznamu. Jakmile máme nový index, vložíme novou instanci třídy SnakeHighscore, používající aktuální skóre a jméno zadané hráčem. Poté odebereme všechny nechtěné záznamy z dolní části seznamu, pokud seznam náhle obsahuje více položek, než chceme (MaxHighscoreListEntryCount). Pak uložíme seznam (SaveHighscoreList()) a skryjeme kontejner bdrNewHighscore, přepínáme zobrazení na kontejner bdrHighscoreList.

Ale stále zbývají ještě nějaké věci k udělání. Za prvé, tyto nové obrazovky (zpráva o smrti, seznam nejlepších výsledků atd.) musí být skryty pokaždé, když začíná nová hra. Takže začátek metody StartNewGame(), kterou jsme implementovali v předchozím článku, by nyní měl vypadat takto:

private void StartNewGame()
{
    bdrWelcomeMessage.Visibility = Visibility.Collapsed;
    bdrHighscoreList.Visibility = Visibility.Collapsed;
    bdrEndOfGame.Visibility = Visibility.Collapsed;
    ........

Další věcí, kterou musíme udělat, je upravit metodu EndGame(). Místo zobrazení MessageBoxu musíme zkontrolovat, zda uživatel právě vstoupil do seznamu nejlepších výsledků nebo ne, a pak zobrazit odpovídající kontejner zpráv:

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;
}

Metoda v základu kontroluje, zda je ještě k dispozici místo v seznamu nejlepších výsledků (definujeme maximálně 5 položek) nebo zda uživatel právě porazil jedno z existujících skóre - pokud ano, umožníme uživateli přidat své jméno tím, že zobrazíme kontejner bdrNewHighscore. Pokud nebylo dosaženo nového nejlepšího výsledku, místo toho zobrazíme kontejner bdrEndOfGame. Nezapomeňte definovat konstantu MaxHighscoreListEntryCount:

const int MaxHighscoreListEntryCount = 5;

Také jsem upravil metodu ContentRendered() - nyní, když máme pěknou uvítací obrazovku, nechceme, aby se hra spustila automaticky. Místo toho naléháme na uživatele, aby stiskl mezerník pro spuštění hry nebo klikl na tlačítko pro zobrazení seznamu nejlepších výsledků před spuštěním hry, takže jednoduše odstraníme (nebo zakomentujeme) volání metody StartNewGame():

private void Window_ContentRendered(object sender, EventArgs e)
{
    DrawGameArea();
    //StartNewGame();
}

S tímto vším na svém místě spusťte hru a dejte do ní všechno nejlepší - jakmile hra skončí, doufejme, že se dostanete do vašeho zbrusu nového seznamu nejlepších výsledků ve hře SnakeWPF!

Shrnutí

V tomto článku jsme provedli HODNĚ vylepšení naší implementace SnakeWPF. Nejzřejmější je samozřejmě seznam nejlepších výsledků, který vyžadoval docela dost dalšího značkování/kódu, ale stálo to za to! Kromě toho jsme provedli některá pěkná vylepšení použitelnosti a znovu jsme našemu projektu dali vzhled opravdové hry.


This article has been fully translated into the following languages: Is your preferred language not on the list? Click here to help us translate this article into your language!