Змейка
Lighting/Colors
Создадим мини-игру змейка.
Для хранения змейки воспользуемся однонаправленным связанным списком. Объявим класс SnakeNode, который будет хранить координаты текущего элемента змейки и указатель на следующий элемент. Использование односвязанного списка удобно по ряду причин которые будут рассмотрены ниже.
public class SnakeNode
{
public SnakeNode Next; // указатель на следующий элемент змейки
// координаты
public int X;
public int Y;
}
Для яблока объявим класс Apple
public class Apple
{
public int X;
public int Y;
}
Обновим перечисление MoveDirection которое будет определять сторону движения головы змейки на следующем ходу
public enum MoveDirection
{
Left, Right, Top, Bottom
}
Объявим переменные необходимые для работы программы:
const int MaxApples = 10; // количество яблок на поле
int Rows => (int)(ClientRectangle.Height / CellSize); // количество строк
int Cols => (int)(ClientRectangle.Width / CellSize); // количество столбцов
bool gameOver = false; // флаг окончания игры
SnakeNode Head = new SnakeNode(); // голова змейки
public List Apples = new List(); // список всех яблок
MoveDirection dir; // текущее направление змейки
float CellSize = 30; // размер ячейки нашей игры
Random Random = new Random(); // генератор случайных чисел
Сделаем функцию NewApple которая будет генерировать новое яблоко на игровом поле. Воспользуемся циклом с постусловием do..while. Случайным образом найдем позицию на карте, которая не занята ни змейкой ни другим яблоком и добавим новое яблоко в список яблок Apples.
public void NewApple() //сгенерировать новое яблоко и добавить его на карту
{
int x = 0;
int y = 0;
do
{
x = Random.Next(1, Cols - 1);
y = Random.Next(1, Rows - 1);
}
while (Apples.Any(z => z.X == x && z.Y == y) || SnakeHas(x, y));
Apples.Add(new Apple() { X = x, Y = y });
}
private bool SnakeHas(int x, int y) // функция проверки что данная клектка поля занята змейкой
{
var node = Head;
while (node != null)
{
if (node.X == x && node.Y == y)
return true;
node = node.Next;
}
return false;
}
Добавим функцию проверки на пересечения новой предполагаемой позиции головы. Сперва проверим на пересечение с границей лабиринта, далее проверим что новая голова не будет пересекаться ни с одним из элементов змейки, кроме последнего, т.к. последний элемент сдвинется при соврешении хода.
private bool CheckCollisions(SnakeNode head)
{
if (head.X == 0 || head.Y == 0 || head.X == Cols - 1 || head.Y == Rows - 1)
return true;
var node = Head;
while (node != null)
{
if (node.X == head.X && node.Y == head.Y)
return true;
node = node.Next;
}
return false;
}
Добавим обработчик кнопок , для этого перегрузим встроенную в Form функцию ProcessCmdKey
protected override bool ProcessCmdKey(ref Message msg, Keys keyData)
{
switch (keyData)
{
case Keys.Left:
if (dir != MoveDirection.Right)
dir = MoveDirection.Left;
break;
case Keys.Right:
if (dir != MoveDirection.Left)
dir = MoveDirection.Right;
break;
case Keys.Up:
if (dir != MoveDirection.Bottom)
dir = MoveDirection.Top;
break;
case Keys.Down:
if (dir != MoveDirection.Top)
dir = MoveDirection.Bottom;
break;
}
return base.ProcessCmdKey(ref msg, keyData);
}
Рассмотри функцию обновления сцены. Создадим новый элмент змейки, который будет новой головой в случае успеха. Если змейка врезалась в препятствие то игра окончена, выставим флаг gameOver. Иначе, заменим Head на новый элемент и сошлемся на старую голову змейки. Как было сказано ранее для хранения змейки удобно использовать односвязанный список. В частности для сдвига змейки на одну клетку вперед достаточно создать новый SnakeNode задав для него ссылку на следующий элемент Next равный старому Head. И обновить указатель Head на вновь созданный элемент SnakeNode. При перемещении змейки все элементы остаются на своих местах кроме первого и последних элментов. Последний элемент должнен быть удален, для этого нужно пройтись до конца односвязанного списка и удалить последний элемент путем задания предпоследнему элементу Next = null.
SnakeNode newHead = new SnakeNode(); // создадим новый узел для предполагаемой позиции головы змейки на следующем ходу
// в начале зададим новой голове координаты старой головы
newHead.X = Head.X;
newHead.Y = Head.Y;
switch (dir) // проверим в какую сторону аправлена голова змейки и сделаем соответствующее приращение координат
{
case MoveDirection.Left:
newHead.X--;
break;
case MoveDirection.Right:
newHead.X++;
break;
case MoveDirection.Top:
newHead.Y--;
break;
case MoveDirection.Bottom:
newHead.Y++;
break;
}
if (CheckCollisions(newHead)) // теперь проверим пересечение новой позиции головы с объектами на карте
{
gameOver = true; // если произошло столкновение то выставим флаг окончания игры
return;
}
// если столкновения не произошло
newHead.Next = Head; // свяжем новый элемент головы со страрой головой
Head = newHead; // обнови указатель Head начала змейки на новый
После того как мы добавили новую голову змейки, нам нужно решить что мы будем делать с последним элементом змейки. Возможны два варианта, либо мы съели яблоко на текущем ходу, и тогда нам не треуется удалять хвост змейки. В противном случае если мы не съели яблоко то мы должны удалить хвости змейки. Для этого дойдем до предпоследнего элемента змейки и сотрем ему Next задав его равным null. Тем самым отсоеденим хвост змейки оставив его без ссылок.
bool removeLastNode = true;
if (Apples.Any(z => z.X == Head.X && z.Y == Head.Y))
{
Apples.RemoveAll(z => z.X == Head.X && z.Y == Head.Y);
NewApple();
removeLastNode = false;
}
if (!removeLastNode)
return;
//remove last node
var node = Head;
while (true)
{
if (node.Next != null && node.Next.Next == null)
{
node.Next = null;
break;
}
node = node.Next;
}
Добавим функцию сброса игры ResetGame
private void ResetGame()
{
Apples.Clear();
Head = new SnakeNode();
gameOver = false;
Head.X = (int)(ClientRectangle.Width / CellSize / 2);
Head.Y = (int)(ClientRectangle.Height / CellSize / 2);
var w = (int)(ClientRectangle.Width % CellSize);
var h = (int)(ClientRectangle.Height % CellSize);
Size = new Size(Size.Width - w, Size.Height - h);
dir = MoveDirection.Left;
for (int i = 0; i < MaxApples; i++)
NewApple();
}
Добавим функции отрисовки всех элементов нашей игры:
private void DrawSnake(Graphics gr) // нарисовать всю змейку целиком
{
DrawNode(Head, gr, CellSize, Brushes.Orange);
var node = Head.Next;
while (node != null)
{
DrawNode(node, gr, CellSize * 0.9f, Brushes.LightGreen);
node = node.Next;
}
}
public void DrawNode(SnakeNode node, Graphics gr, float size, Brush brush) // нарисовать элемент змейки
{
gr.DrawEllipse(Pens.Black, node.X * CellSize + (CellSize - size) / 2, node.Y * CellSize + (CellSize - size) / 2, size, size);
gr.FillEllipse(brush, node.X * CellSize + (CellSize - size) / 2, node.Y * CellSize + (CellSize - size) / 2, size, size);
}
public void DrawWall(Graphics gr, int x, int y) //нарисовать один элемент стены
{
gr.DrawRectangle(Pens.Black, x * CellSize, y * CellSize, CellSize, CellSize);
gr.FillRectangle(Brushes.Thistle, x * CellSize, y * CellSize, CellSize, CellSize);
}
public void DrawAllWalls(Graphics gr) // нарисовать всю стену целиком
{
for (int i = 0; i < Cols; i++)
{
DrawWall(gr, i, 0);
DrawWall(gr, i, Rows - 1);
}
for (int i = 0; i < Rows; i++)
{
DrawWall(gr, 0, i);
DrawWall(gr, Cols - 1, i);
}
}
Запустив данный пример мы увидим следующую анимацию:

Исходный код данного примера вы можете найти здесь