Ссылка на проект в GitHub.
В данной статье хочу разобрать создание змейки.
Первое с чего нужно начать это создать свой класс игры и унаследовать от базового класса Game.
class Snake : Game
в нём уже реализовано игровое поле и события которые возникают при переходе игры из одного состояния в другое. По сути всё что нам нужно сделать это объявить обработку событий.
public Snake() : base() { OnPreview += BasePreview; OnNewGame += Snake_OnNewGame; OnUpdateGame += Snake_OnUpdateGame; OnGameOver += DrawScore; }
Для событий OnPreview и OnGameOver уже есть готовые заглушки в классе Game их можно не реализовывать. Остаётся только инициализировать новую игру и обработать события обновления.
private GameBlock head; private List<GameBlock> body; private GameBlock eat; private void Snake_OnNewGame() { head = new GameBlock() { X = 10, Y = 10, Vector = Vector.Up, Color = GameColor.Green }; body = new List<GameBlock>(); body.Add( head ); body.Add( new GameBlock() { X = 10, Y = 11, Vector = Vector.Up, Color = GameColor.Black } ); body.Add( new GameBlock() { X = 10, Y = 12, Vector = Vector.Up, Color = GameColor.Black } ); CreateEat(); DrawField(); }
Для отрисовки поля можно работать с ним напрямую, а можно использовать уже готовый класс GameBlock в нём реализованы такие вещи как положение, направление движения и цвет.
В данной функции мы объявили тело змейки, создаём первый кусочек еды и выводим происходящие на поле.
private void CreateEat() { var emptyBlocks = new List<GameBlock>(); for( int i = 0; i < MainForm.FIELD_SIZE; i++ ) for( int j = 0; j < MainForm.FIELD_SIZE; j++ ) if( CheckEmptyBlock( i, j ) ) emptyBlocks.Add(new GameBlock() { X = i, Y = j, Color = GameColor.Red } ); if (emptyBlocks.Count > 0) eat = emptyBlocks[random.Next( emptyBlocks.Count )]; }
Для создания еды мы получаем список пустых блоков и с помощью рандомизатора (который уже объявлен в Game) выбираем случайный. На случай если змейка заняла всё поле стоит проверка на размер списка.
Собственно, функция проверки пустой клетки:
private bool CheckEmptyBlock(int x, int y) => !( x < 0 || y < 0 || x == MainForm.FIELD_SIZE || y == MainForm.FIELD_SIZE ) && !body.Exists( a => a.Equals( new GameBlock() { X = x, Y = y } ) );
Отрисовка поля выглядит следующим образом:
private void DrawField() { Field.Clear( GameColor.White ); Field.DrawGameBlock( eat ); Field.DrawGameBlocks( body ); WriteScore(); }
Как не трудно догадаться, поле очищается белым цветом и выводятся еда со змеёй. WriteScore ещё одна стандартная функция для вывода счёта в специальную строку состояния.
Итак переходим к событию обновления игры, которое происходит с периодичностью в 300 мс.
private void Snake_OnUpdateGame( Controller controller ) { ControlMove( controller.GameKey ); if( CheckGameOver() ) GameOver(); else SnakeMove(); }
В нём происходит четыре вещи: изменения направления движения, проверка на конец игры, вызов события конца игры и перемещении змеи в случае, если всё в порядке.
private void ControlMove( GameKey key ) { switch( key ) { case GameKey.Left: head.Vector = head.Vector == Vector.Right ? Vector.Right : Vector.Left; break; case GameKey.Right: head.Vector = head.Vector == Vector.Left ? Vector.Left : Vector.Right; break; case GameKey.Up: head.Vector = head.Vector == Vector.Down ? Vector.Down : Vector.Up; break; case GameKey.Down: head.Vector = head.Vector == Vector.Up ? Vector.Up : Vector.Down; break; default: break; } }
Чтобы изменить направление движения в змейке нам нужно поменять вектор в её голове. Поэтому в контроле движения есть проверка на случай инверсии вектора, для того чтобы змейка не начала залезать сама на себя.
private bool CheckGameOver() { switch( head.Vector ) { case Vector.Up: return !CheckEmptyBlock( head.X, head.Y - 1 ); case Vector.Down: return !CheckEmptyBlock( head.X, head.Y + 1 ); case Vector.Left: return !CheckEmptyBlock( head.X - 1, head.Y ); case Vector.Right: return !CheckEmptyBlock( head.X + 1, head.Y ); default: throw new NotImplementedException(); } }
Для проверки конца игры достаточно проверить является ли блок по направлению свободным или нет. Как можно догадаться еда в проверке игнорируется.
Осталось разобрать функцию передвижения змейки:
private void SnakeMove() { var temp = body.Last().Copy(); foreach( var block in body ) block.Move(); for( int i = body.Count - 1; i > 0; i-- ) body[i].Vector = body[i - 1].Vector; if( head.Equals( eat ) ) { score++; body.Add( temp ); CreateEat(); } DrawField(); }
Конец хвоста копируется для того чтобы в случае, если была достигнута еда добавить его как наращение змеи. Передвинуть блоки не составляет труда, потому что в классе блока уже реализована эта функция. Затем происходит распределение векторов по движению змеи и проверка на пересечение с едой. Если еда найдена счёт инкрементируется, змея увеличивается и создаётся новая еда. Для того чтобы наша игра отобразилась в списке игр, её нужно добавить в инициализацию формы:
List<Game> games = new List<Game>(); games.Add( new Snake() ); games.Add( new Tetris() ); games.Add( new Life() ); Application.Run( new MainForm( games ) );
Вот собственно и всё. Весь код игры занял всего 102 строчки. Как можно увидеть из примера в проект уже добавлены тетрис и игра жизнь. Ниже можно ознакомиться с получившемся результатом.

Меню выбора игры

Процесс игры

Конец игры

