...

четверг, 18 апреля 2019 г.

[Из песочницы] GameBoy на C#

Когда я только начинал программировать, думаю, как и многим, мне хотелось делать игры. Но передо мной стояло множество архитектурных вопросов, которые я не знал как решить, про двойную буферизацию я даже не слышал, а получить результат хотелось как можно скорее. Поэтому недавно я решил написать проект, в котором можно будет писать простенькие игры без каких-либо проблем. Игры в этом проекте можно создавать по типу GameBoy, то есть: тетрис, змейка и т.д. Но кликать мышкой в нём тоже можно.

Ссылка на проект в 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 строчки. Как можно увидеть из примера в проект уже добавлены тетрис и игра жизнь. Ниже можно ознакомиться с получившемся результатом.

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

image
Процесс игры

image
Конец игры

Let's block ads! (Why?)

Комментариев нет:

Отправить комментарий