TOC

This article has been localized into Russian by the community.

Создание игры: WPF змейка:
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!

Создание & перемещение змейки

В предыдущей статье мы создали игровое поле по которому будет перемещаться наша змейка. Теперь пришло время создать саму змейку и заставить её двигаться по полю. Опять таки, мы будем использовать WPF Rectangle объекты, чтобы сформировать змейку определённой длины, у которой каждый фрагмент будет иметь высоту и ширину квадратиков на заднем плане, то есть его измерения будут равны константе SnakeSquareSize.

Создание змейки

Мы будем рисовать змейку методом DrawSnake() - там не будет ничего сложного, однако для его реализации нам понадобится ещё пара вещей, а конкретно: новый класс SnakePart, а также несколько новых полей в классе Window. Начнём с определения класса SnakePart, что следует делать в новом файле (например: 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; }
    }
}

Этот несложный класс будет содержать информацию о каждом фрагменте змейки: где он находится, какой UIElement (в нашем случае - Rectangle) его изображает, и является ли он головой змейки. Мы воспользуемся этим позже, а пока нам нужно добавить несколько полей внутри класса Window, которые будут использоваться в нашем методе DrawSnake() (а позже и в других методах):

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>();  
    ......

Мы добавляем две цветовых кисти SolidColorBrush, одну для тела и другую для головы. Также мы определяем список List<SnakePart>, который будет хранить ссылки на каждый из фрагментов змейки. Теперь, мы можем приступать к реализации метода 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);
}
    }
}

Как видите, в этом методе нет ничего сложного: мы обрабатываем список snakeParts в цикле, и для каждого фрагмента мы проверяем, был ли для него назначен UIElement - и если нет, мы создаём прямоугольник и добавляем к игровому полю, не забывая сохранить ссылку на него в экземпляре SnakePart в свойстве UiElement. Заметьте, как мы используем свойство Position класса SnakePart для позиционирования самих элементов внутри Canvas'а.

Здесь фокус в том, что фрагменты змейки будут определены в другом месте, что позволят нам добавлять один или несколько фрагментов к змейке, задавать им позицию, а затем вызывать метод DrawSnake(), который сделает всё остальное за нас. Мы оформим это как часть процесса перемещения змейки.

Перемещение змейки

Чтобы метод DrawSnake() заработал, нам нужно заполнить список snakeParts. Этот список отвечает за информацию о том, где нужно рисовать каждый фрагмент змейки, поэтому мы можем также использовать его для реализации передвижения. В целом, процесс перемещения змейки состоит из добавления нового фрагмента по направлению движения, и удаления хвостового фрагмента. Это будет выглядеть так, как будто каждый фрагмент действительно движется, но на самом деле мы лишь добавляем новые фрагменты и удаляем старые.

Так, нам нужен метод MoveSnake(), которым мы сейчас и займёмся, но сперва следует добавить кое-что в определение класса Window:

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

Мы добавили новое перечисление(enum) SnakeDirection для представления направления движения. Нам понадобится приватное поле в котором мы будем хранить текущее направление змейки (snakeDirection), а также целочисленная переменная для хранения её длины (snakeLength). Теперь мы готовы к реализации метода MoveSnake(). Он получился весьма объёмным, так что я добавил к нему комментарии:

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

Теперь у нас имеется вся необходимая логика для перемещения змейки. Заметьте, как часто мы используем константу SnakeSquareSize - от рисования клетчатого фона до реализации передвижения.

Заключение

В первой статье мы добавили фон, а в этой - код для движения змейки. Но поскольку ни один из реализованных методов не вызывается из кода, у нас всё ещё нет ни движения, ни самой змейки на игровом поле.

Вызовы к методу передвижения должны поступать постоянно, так как змейка движется безостановочно на протяжении всей игры. Для этого в WPF есть класс DispatcherTimer. Использование таймера для создания постоянного движения змейки - тема следующей статьи.

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!