TOC

This article is currently in the process of being translated into Polish (~99% done).

Tworzenie Gry: 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!

Creating & moving the Snake

W poprzednim artykule wykonaliśmy obszar, po którym będziemy poruszać się naszym wężem. Teraz nadszedł czas na stworzenie samego węża oraz umożliwienie poruszania się nim. Po raz kolejny skorzystam z kontrolki WPF Rectangle do stworzenia węża określonej długości, z każdym segmentem o wielkości dokładnie takiej samej, jak kwadraty naszej szachownicy w tle, dzięki naszej stałej SnakeSquareSize.

Tworzenie węża

Narysujemy naszego węża, dzięki metodzie nazwanej DrawSnake() - metoda sama w sobie jest dość prosta, jednak wymaga kilku dodatkowych rzeczy. Jedną z nich jest nowa klasa nazwana SnakePart, oraz kilka dodatkowych pól w klasie Okno. Zacznijmy od klasy SnakePart, którą stworzymy w nowym pliku (na przykład SnakePart.cs):

using System.Windows;

namespace WpfTutorialSamples.Games
{
    public class SnakePart
    {
public UIElement UiElement { get; set; }

public Point Position { get; set; }

public bool IsHead { get; set; }
    }
}

Ta prosta klasa będzie zawierała informacje na temat każdego z fragmentów węża: Gdzie dany fragment się znajduje w obszarze gry, który UIElement (w naszym przypadku Rectangle) reprezentuje dany fragment, oraz parametr bool określający czy dany fragment jest głową czy nie? Będziemy korzystać z tego później, na tę chwilę musimy zdefiniować w klasie Okno kilka dodatkowych pól, których będziemy używać w metodzie DrawSnake() (oraz w późniejszym czasie również w innych metodach):

public partial class SnakeWPFSample : Window  
{  
    const int SnakeSquareSize = 20;  

    private SolidColorBrush snakeBodyBrush = Brushes.Green;  
    private SolidColorBrush snakeHeadBrush = Brushes.YellowGreen;  
    private List<SnakePart> snakeParts = new List<SnakePart>();  
    ......

Zdefiniowaliśmy dwa SolidColorBrushe, jedną dla ciała oraz jedną dla głowy. Zdefiniowaliśmy także Listę<SnakePart>, która będzie zawierała referencje do wszystkich fragmentów ciała węża. Teraz możemy zaimplementować naszą klasę DrawSnake():

private void DrawSnake()
{
    foreach(SnakePart snakePart in snakeParts)
    {
if(snakePart.UiElement == null)
{
    snakePart.UiElement = new Rectangle()
    {
Width = SnakeSquareSize,
Height = SnakeSquareSize,
Fill = (snakePart.IsHead ? snakeHeadBrush : snakeBodyBrush)
    };
    GameArea.Children.Add(snakePart.UiElement);
    Canvas.SetTop(snakePart.UiElement, snakePart.Position.Y);
    Canvas.SetLeft(snakePart.UiElement, snakePart.Position.X);
}
    }
}

Jak widać, metoda ta nie jest jakoś mocno skomplikowana: Wykonujemy pętlę po liście z fragmentami węża snakeParts, i dla każdego z nich sprawdzamy czy posiada zdefiniowany parametr UIElement - jeśli nie tworzymy go (w naszym przypadku Rectangle) i dodajemy do obszaru gry, jednocześnie zapisując referencje do tego fragmentu we właściwości UiElement. Zwróć uwagę, jak używamy właściwości Position instancji SnakePart, aby umieścić rzeczywisty element wewnątrz GameArea Canvas.

Cała sztuczka polega na tym, że rzeczywiste fragmenty węża będą zdefiniowane gdzie indziej, pozwalając nam na dodanie jednej lub kilku fragmentów do węża, zdefiniowanie ich położenia, a następnie dzięki metodzie DrawSnake() narysowanie ich. Zrobimy to w ramach tego samego procesu, który służy do przemieszczenia węża.

Poruszanie węża

Aby metoda DrawSnake() działała tak, jak tego chcemy musimy wypełnić naszą listę snakeParts. Ta lista stale służy jako podstawa do rysowania każdego fragmentu węża, więc użyjemy jej również do stworzenia jego ruchu. Cały proces poruszania w zasadzie opiera się na dodaniu nowego elementu, w kierunku w którym wąż się obecnie porusza, a następnie usunięciu ostatniego z fragmentów ciała. Dzięki temu będzie to wyglądać jakby wąż się poruszał, jednak tak na prawdę po prostu dodajemy nowe fragmenty i usuwamy stare.

Zatem, będziemy potrzebowali metody MoveSnake(), którą pokażę ci już za chwilę, ale najpierw dodamy trochę kodu do definicji głównego okna:

public partial class SnakeWPFSample : Window  
{  
    const int SnakeSquareSize = 20;  

    private SolidColorBrush snakeBodyBrush = Brushes.Green;  
    private SolidColorBrush snakeHeadBrush = Brushes.YellowGreen;  
    private List<SnakePart> snakeParts = new List<SnakePart>();  

    public enum SnakeDirection { Left, Right, Up, Down };  
    private SnakeDirection snakeDirection = SnakeDirection.Right;  
    private int snakeLength;  
    ......

Dodaliśmy kilka enumeracji nazwanych SnakeDirection, których nazwa powinna mówić sama za siebie. Stworzyliśmy prywatne pole, w którym będzie przetrzymywany obecny kierunek poruszania się węża (snakeDirection), następnie stworzyliśmy zmienną typu integer do przechowywania aktualnej długości węża (snakeLength). Gdy mamy już to wszystko możemy zaimplementować metodę MoveSnake(). Jest ona dość długa, dlatego przy ważniejszych częściach dodane są komentarze.

private void MoveSnake()  
{  
    // Remove the last part of the snake, in preparation of the new part added below  
    while(snakeParts.Count >= snakeLength)  
    {  
GameArea.Children.Remove(snakeParts[0].UiElement);  
snakeParts.RemoveAt(0);  
    }  
    // Next up, we'll add a new element to the snake, which will be the (new) head  
    // Therefore, we mark all existing parts as non-head (body) elements and then  
    // we make sure that they use the body brush  
    foreach(SnakePart snakePart in snakeParts)  
    {  
(snakePart.UiElement as Rectangle).Fill = snakeBodyBrush;  
snakePart.IsHead = false;  
    }  

    // Determine in which direction to expand the snake, based on the current direction  
    SnakePart snakeHead = snakeParts[snakeParts.Count - 1];  
    double nextX = snakeHead.Position.X;  
    double nextY = snakeHead.Position.Y;  
    switch(snakeDirection)  
    {  
case SnakeDirection.Left:  
    nextX -= SnakeSquareSize;  
    break;  
case SnakeDirection.Right:  
    nextX += SnakeSquareSize;  
    break;  
case SnakeDirection.Up:  
    nextY -= SnakeSquareSize;  
    break;  
case SnakeDirection.Down:  
    nextY += SnakeSquareSize;  
    break;  
    }  

    // Now add the new head part to our list of snake parts...  
    snakeParts.Add(new SnakePart()  
    {  
Position = new Point(nextX, nextY),  
IsHead = true  
    });  
    //... and then have it drawn!  
    DrawSnake();  
    // We'll get to this later...  
    //DoCollisionCheck();      
}

Dzięki temu mamy teraz całą logikę potrzebną do poruszania naszego węża. Zauważ, że nasza stała SnakeSquareSize jest używana we wszystkich aspektach gry - od rysowania tła, poprzez tworzenie oraz dodawanie nowych fragmentów węża.

Podsumowanie

W pierwszym artykule opisaliśmy jak stworzyć tło, w tym w jaki sposób tworzyć, modyfikować oraz poruszać wężem. Jednak pomimo tej całej logiki, nadal w grze nie uświadczymy poruszania ani nawet samego węża w obszarze gry. Wynika to z tego, że póki co nie wywołaliśmy żadnej z tych metod.

Wywoływanie metody odpowiedzialnej za poruszanie się węża musi pochodzić z powtarzajacego się źródła, ponieważ wąż powinien się bez przerwy poruszać. W WPF istnieje klasa DispatcherTimer, która nam w tym pomoże. W następnym artykule opiszemy w jaki sposób osiągnąć ciągły ruch, dzięki zegarowi zawartemu w tej klasie.


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!