Когда я только начинал программировать, думаю, как и многим, мне хотелось делать игры. Но передо мной стояло множество архитектурных вопросов, которые я не знал как решить, про двойную буферизацию я даже не слышал, а получить результат хотелось как можно скорее. Поэтому недавно я решил написать проект, в котором можно будет писать простенькие игры без каких-либо проблем. Игры в этом проекте можно создавать по типу GameBoy, то есть: тетрис, змейка и т.д. Но кликать мышкой в нём тоже можно.
Ссылка на проект в GitHub.
В данной статье хочу разобрать создание змейки.
Первое с чего нужно начать это создать свой класс игры и унаследовать от базового класса Game.
в нём уже реализовано игровое поле и события которые возникают при переходе игры из одного состояния в другое. По сути всё что нам нужно сделать это объявить обработку событий.
Для событий OnPreview и OnGameOver уже есть готовые заглушки в классе Game их можно не реализовывать. Остаётся только инициализировать новую игру и обработать события обновления.
Для отрисовки поля можно работать с ним напрямую, а можно использовать уже готовый класс GameBlock в нём реализованы такие вещи как положение, направление движения и цвет.
В данной функции мы объявили тело змейки, создаём первый кусочек еды и выводим происходящие на поле.
Для создания еды мы получаем список пустых блоков и с помощью рандомизатора (который уже объявлен в Game) выбираем случайный. На случай если змейка заняла всё поле стоит проверка на размер списка.
Собственно, функция проверки пустой клетки:
Отрисовка поля выглядит следующим образом:
Как не трудно догадаться, поле очищается белым цветом и выводятся еда со змеёй. WriteScore ещё одна стандартная функция для вывода счёта в специальную строку состояния.
Итак переходим к событию обновления игры, которое происходит с периодичностью в 300 мс.
В нём происходит четыре вещи: изменения направления движения, проверка на конец игры, вызов события конца игры и перемещении змеи в случае, если всё в порядке.
Чтобы изменить направление движения в змейке нам нужно поменять вектор в её голове. Поэтому в контроле движения есть проверка на случай инверсии вектора, для того чтобы змейка не начала залезать сама на себя.
Для проверки конца игры достаточно проверить является ли блок по направлению свободным или нет. Как можно догадаться еда в проверке игнорируется.
Осталось разобрать функцию передвижения змейки:
Конец хвоста копируется для того чтобы в случае, если была достигнута еда добавить его как наращение змеи. Передвинуть блоки не составляет труда, потому что в классе блока уже реализована эта функция. Затем происходит распределение векторов по движению змеи и проверка на пересечение с едой. Если еда найдена счёт инкрементируется, змея увеличивается и создаётся новая еда. Для того чтобы наша игра отобразилась в списке игр, её нужно добавить в инициализацию формы:
Вот собственно и всё. Весь код игры занял всего 102 строчки. Как можно увидеть из примера в проект уже добавлены тетрис и игра жизнь. Ниже можно ознакомиться с получившемся результатом.
Меню выбора игры
Процесс игры
Конец игры
Ссылка на проект в 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 строчки. Как можно увидеть из примера в проект уже добавлены тетрис и игра жизнь. Ниже можно ознакомиться с получившемся результатом.
Меню выбора игры
Процесс игры
Конец игры