Статья
Версия для печати
Обсудить на форуме
Советы по Windows (7.2)

3. Разработка  игрового интерфейса

Игровой интерфейс включает в себя:
  • а) графическое отображение на экране
  • б) пользовательское управление
  • в) сам алгоритм игры.
Для отработки каждого действия на игровом поле в нашем случае необходимо будет работать с графическими объектами, которые в Windows вполне неплохо и понятно сделаны.
Нам понадобиться:
  • а) отобразить игровое поле пустым в начале.
  • б) отображать объекты типа блок (ABlock) на игровм поле.
  • г) отображать передвижение и поворот объекта ABlock.
  • д) отображать оставшиеся объекты в стакане в виде занятых ячеек.
  • е) отображать процесс уничтожения объектов на поле, когда вся линия в стакане заполнена, и сдвиг всех занятых клеток вниз, после уничтожения полных строк.
Для отображения простой картинки в Windows используют так называемый Device context. В MFC предусмотрено несколько объектов  для работы с ними. Это CDC - базовый объект, CClientDC - используется для работы с клиентской частью окна (белый фон) CWindowDC - полностью все окно, можно накладывать рисунок даже на область шапки, меню и т.д., и CPaintDC который используется программой для собственно вывода в область окна CClientDC в процедуре OnPaint.
OnPaint - процедура обработки соробщения WM_PAINT которое посылается окну каждый раз, когда система считает необходимым перерисовать окно программы (после команды минимизировать или максимизировать окно например), или когда вы вызываете функцию Invalidate();
Таким образом, для постоянного отображения графического контекста на экране, нам необходимо рисовать всю информацию в процедуре OnPaint().
Для нас, немного сложно сразу перейти к разработке полного игрового интерфейса, поэтому переместимся немного в код, и доработаем его под наши нужды...

3.1 Подготовка объекта игры.

Для нас важно теперь создать некий суммирующий объект в котором мы опишем основные обработчики событий происходящих в нашей программе. Создадим класс ATetris.
Код: (C++)
#include "ABlock.h"
#include "APlace.h"

class ATetris
{
public:
        ATetris(void);
        ~ATetris(void);
        bool Init(void);
        void DeInit(void);

private:
        APlace m_Place;
        ABlock * pBlock[7];
};
Как видно мы создаем объект, который будет управлять всеми ранее созданными объектами, и будет заниматься всей игровой задачей.
Обратите внимание, что сразу вводим функцию Init - которая будет инициализировать не что иное как указатели на объекты типа ABlock...
Почему в данном случае удобнее использовать указатели, вы увидите из текста функции Init();
Код: (C++)
ATetris::ATetris(void)
{
        pBlock[0] = NULL;
        pBlock[1] = NULL;
        pBlock[2] = NULL;
        pBlock[3] = NULL;
        pBlock[4] = NULL;
        pBlock[5] = NULL;
        pBlock[6] = NULL;
       
}

ATetris::~ATetris(void)
{
       
}

bool ATetris::Init()
{
        bool ret = true;
        TRIS_BLOCK tb;
        // Init blocks from numbers//
        while(1)
        {
                // #1

        int a[4][4][4]={0,1,0,0,
                        0,1,0,0,
                        0,1,1,0,
                        0,0,0,0,

                        0,0,0,0,
                        0,1,1,1,
                        0,1,0,0,
                        0,0,0,0,

                        0,1,1,0,
                        0,0,1,0,
                        0,0,1,0,
                        0,0,0,0,

                        0,0,0,0,
                        0,0,1,0,
                        1,1,1,0,
                        0,0,0,0};
               
        memcpy(&tb.block,&a,sizeof(int)*4*4*4);
                pBlock[0] = new ABlock(tb);
                if (!pBlock[0]) {
                        ret=false;
                        break;
                }
                // #2
                int b[4][4][4]={0,0,1,0,
                                0,0,1,0,
                                0,1,1,0,
                                0,0,0,0,

                                0,0,0,0,
                                0,1,0,0,
                                0,1,1,1,
                                0,0,0,0,
                               
                                0,1,1,0,
                                0,1,0,0,
                                0,1,0,0,
                                0,0,0,0,

                                0,0,0,0,
                                1,1,1,0,
                                0,0,1,0,
                                0,0,0,0};
               
        memcpy(&tb.block,&b,sizeof(int)*4*4*4);
                pBlock[1] = new ABlock(tb);
                if (!pBlock[1]) {
                        ret=false;
                        break;
                }
                // #3
        int c[4][4][4]={0,1,0,0,
                        0,1,0,0,
                        0,1,0,0,
                        0,1,0,0,

                        0,0,0,0,
                        1,1,1,1,
                        0,0,0,0,
                        0,0,0,0,

                        0,1,0,0,
                        0,1,0,0,
                        0,1,0,0,
                        0,1,0,0,

                        0,0,0,0,
                        1,1,1,1,
                        0,0,0,0,
                        0,0,0,0};
               
                memcpy(&tb.block,&c,sizeof(int)*4*4*4);
                pBlock[2] = new ABlock(tb);
                if (!pBlock[2]) {
                        ret=false;
                        break;
                }
                // #4
        int d[4][4][4]={0,0,0,0,
                        0,1,1,0,
                        0,1,1,0,
                        0,0,0,0,

                        0,0,0,0,
                        0,1,1,0,
                        0,1,1,0,
                        0,0,0,0,

                        0,0,0,0,
                        0,1,1,0,
                        0,1,1,0,
                        0,0,0,0,

                        0,0,0,0,
                        0,1,1,0,
                        0,1,1,0,
                        0,0,0,0 };
               
                memcpy(&tb.block,&d,sizeof(int)*4*4*4);

                pBlock[3] = new ABlock(tb);
                if (!pBlock[3]) {
                        ret=false;
                        break;
                }
                // #5
        int e[4][4][4]={0,1,0,0,
                        0,1,1,0,
                        0,0,1,0,
                        0,0,0,0,

                        0,0,0,0,
                        0,0,1,1,
                        0,1,1,0,
                        0,0,0,0,

                        0,1,0,0,
                        0,1,1,0,
                        0,0,1,0,
                        0,0,0,0,
                       
                        0,0,0,0,
                        0,0,1,1,
                        0,1,1,0,
                        0,0,0,0};
               
                memcpy(&tb.block,&e,sizeof(int)*4*4*4);

                pBlock[4] = new ABlock(tb);
                if (!pBlock[4]) {
                        ret=false;
                        break;
                }
                // #6
        int f[4][4][4]={0,0,1,0,
                        0,1,1,0,
                        0,1,0,0,
                        0,0,0,0,

                        0,0,0,0,
                        0,1,1,0,
                        0,0,1,1,
                        0,0,0,0,

                        0,0,1,0,
                        0,1,1,0,
                        0,1,0,0,
                        0,0,0,0,

                        0,0,0,0,
                        0,1,1,0,
                        0,0,1,1,
                        0,0,0,0};
               
                memcpy(&tb.block,&f,sizeof(int)*4*4*4);

                pBlock[5] = new ABlock(tb);
                if (!pBlock[5]) {
                        ret=false;
                        break;
                }
                //#7
        int j[4][4][4]={0,0,0,0,
                        1,1,1,0,
                        0,1,0,0,
                        0,0,0,0,

                        0,0,1,0,
                        0,1,1,0,
                        0,0,1,0,
                        0,0,0,0,

                        0,0,0,0,
                        0,0,1,0,
                        0,1,1,1,
                        0,0,0,0,

                        0,0,0,0,
                        0,1,0,0,
                        0,1,1,0,
                        0,1,0,0};
                memcpy(&tb.block,&j,sizeof(int)*4*4*4);

                pBlock[6] = new ABlock(tb);
                if (!pBlock[6]) {
                        ret=false;
                        break;
                }
                break;
        }

        if (!ret)
        {
                for (int i=0; i<7; i++)
                {
                        if (pBlock[i]!=NULL) delete pBlock[i];
                        pBlock[i]=NULL;
                }
        }

       
        return ret;
}


void ATetris::DeInit(void)
{
        for (int i=0; i<7; i++)
        {
                if (pBlock[i]!=NULL) delete pBlock[i];
                pBlock[i]=NULL;
        }
}
Как видите, сразу, с конструированием объекта вводятся данные блоков в конструктор ABlock.
Хочу обратить внимание, что инициируются сразу 4 позиции каждого блока, даже для квадратного, дабы сохранить принцип единообразия в объектах.

3.2 Подготовка кадров.

Все мы любили или любим мультфильмы. Думаю ни один из вас не прошел мимо интереса, а как же оно все там двигается. Мы займемся формированием именно похожих кадров, которые и будут создавать мультипликацию (очень громко сказано) процесса игры.
Опять отступим немного от игры и посмотрим как происходит вывод картинки на экран в Windows. Самым наглядным способом является работа с запасным Device Context-ом (DC).
Пример кода  (этот код в проект вставлять не надо):
Код: (C++)
CPaintDC dc(this); // Формируется стандартный выводной DC в OnPaint.
CDC cdc; //наш с Вами буферный DC
CBitmap bmp; //все работы с графикой происходят только над битмапками.

сdc.CreateCompatibleDC(&dc); // Конструируем объект как совпадающий с нашим основным по свойствам.
bmp.CreateCompatibleBitmap(&dc,width,height); // создаем соответственно битмапку для отображения изображения совпадающую по свойствам с текущей и с нужными нам размерами.

сdc.SelectObject(bmp); // закидываем битмапку в DC.
В данном случае мы имеем дело с парой объектов, которые связываются между собой. Стоит учесть, что все свойства, как цвет текста или работа с перьями связываются точно также.
Код: (C++)
cdc.Rectangle(CRect(0,0,width,height)); // рисуем текущими параметрами (белое заполнение черные линии) обычный четырехугольник

dc.BitBlt(0,0,width,height,&dcd,0,0,SRCCOPY); // "блитаем" на экран нужную вещь...

// уничтожаем контексты...
bmp.DeleteObject();
cdc.DeleteDC();
Это обычный пример процедуры рисования. Единственной необъясненной вещью остается BitBlt.
Конечно для вышенаписанного примера гораздо легче было бы нарисовать все прямо в dc
Код: (C++)
 
dc.Rectangle();
Но для нашей задачи было важно понять как рисовать кадры в памяти. Это необходимо, для того, что бы избежать постоянного мерцания и чрезмерно загруженной процедуры OnPaint(). Каким бы быстрым не были сегодня компьютеры - глаз успевает заметить чистый - неотрисованный экран - если все рисовать напрямую, получается очень некрасиво.
Теперь о блите. Это процедура битового копирования маски готового кадра с dc в dc. Подготовив в памяти кадр, нарисовав в нем все необходимые нам вещи мы просто выводим быстро на экран все что нам необходимо. Т.е. новое положение мультиплицируемых вещей.
В качестве параметров BitBlt выступают - координаты верхнего левого угла, размеры блитового изображения у исходника, исходный dc точка левого верхнего угла в исходном dc и метод вывода. Все методы вывода можете посмтреть в msdn, а в данном случае SRCCOPY означает простое копирование с затиранием предыдущего изображения.
С теорией покончено, давайте теперь создадим наши кадры для игры... По классической системе планирования нам надо было бы создать для всей графики отдельный объект, импортировать в него данные в виде входных параметров, и таким образом уже формировать все кадры. Но у нас задачка попроще, и я не стал загромождать проект кучей оюъектов. Подойдем к задачке попроще, мы имеем объект поля, который нами будет отрисовываться. Работая с данными мы подготовим формальизованное представление и отформатируем все кадры в объекте поля APlace.
Итак, вернемся к объекту APlace, и добавим в него:
Код: (C++)
#include "resource.h"

public:
        void CreateBasePlace(CDC * pDC);
        void DestroyBasePlace(void);
        CDC * GetBaseDC(void);
        CDC * GetBlockDC(void);
        void CreateGameDC();

private:

        CBitmap m_bmp;
        CDC m_dc;

        CDC blockDC;
        CDC fireDC;
        CBitmap blockBmp;
        CBitmap firedBmp;

        CBitmap m_gameBmp;
        CDC m_gameDC;

        CBitmap m_showBmp;
        CDC m_showDC;
Зачем кто из них пригодится я буду описывать по ходу. А сейчас первым делом мы должны создать и инициировать все рабочие dc и Bmp объекты...
Для этого мы будем использовать функцию void CreateBasePlace(CDC * pDC).
Код: (C++)
void APlace::CreateBasePlace(CDC * pDC)
{
        // Create Buffer DC
        m_dc.CreateCompatibleDC(pDC);
        //Create bitmap for buffer DC
       
        m_bmp.LoadBitmap(IDB_PLACEBMP);
        // Create block DC
        blockDC.CreateCompatibleDC(pDC);
        blockBmp.LoadBitmap(IDB_BLOCKBMP);
        blockDC.SelectObject(blockBmp);

        fireDC.CreateCompatibleDC(pDC);
        firedBmp.LoadBitmap(IDB_BLOCKFIRE);
        fireDC.SelectObject(firedBmp);

        m_dc.SelectObject(m_bmp);

       
        m_showDC.CreateCompatibleDC(pDC);
        m_showBmp.CreateCompatibleBitmap(pDC,MAXX*PIXFORRECT,MAXY*PIXFORRECT);
        m_showDC.SelectObject(m_showBmp);

}
Как видите в этой функции я не только готовлю сами объекты, но и подгружаю IDB_PLACEBMP, IDB_BLOCKBMP, IDB_BLOCKFIRE.
  • IDB_BLOCKFIRE - это я использую изобраение огня, которое появится при исчезновении полных линий.
  • IDB_BLOCKBMP - собственно изображение квадрата (ячейки) для блока и для поля.
  • IDB_PLACEBMP - собственно фон для стакана. У меня просто белый квадрат с названием программы у вас может быть фото себя любимого или еще что на ваш выбор. Размер в данном случае 200х400 - размер стакана. Пока не меняйте его, позже, когда станет понятна вся структура программы вы сможете отредактировать оформление как вам удобнее.
Одновременно озаботимся деинициализацией данных объектов:
Код: (C++)
void APlace::DestroyBasePlace(void)
{
        blockBmp.DeleteObject();
        blockDC.DeleteDC();

        firedBmp.DeleteObject();
        fireDC.DeleteDC();

        m_bmp.DeleteObject();
        m_dc.DeleteDC();

        m_showBmp.DeleteObject();
        m_showDC.DeleteDC();

        memset(Place,0,sizeof(int)*MAXX*MAXY);
}
Таким образом мы получили заготовленные в памяти нужные нам обекты кадров, каждый из которых поможет нам выводить на экран нужные вещи...
Напишем еще пару функций:

Код: (C++)
CDC * APlace::GetBaseDC(void)
{
        return &m_showDC;
}
Просто возврат объекта, его можно использовать например для отладки в процессе программирования выводя его соджержимое напрямую...
Код: (C++)
CDC *  APlace::GetBlockDC(void)
{
        return &blockDC;
}
То же что и в прошлом случае.
Вот теперь самая интересная функция, которая и формирует кадр для блита на главное окно. Для него используется m_showDC.
Код: (C++)
void APlace::CreateGameDC(void);
Давайте посмотрим на код:
Код: (C++)
void APlace::CreateGameDC(void)
{
        // Создадим нужный нам текущий кадр игры.
        m_gameDC.CreateCompatibleDC(&m_dc);
        m_gameBmp.CreateCompatibleBitmap(&m_dc,MAXX*PIXFORRECT,MAXY*PIXFORRECT);
        m_gameDC.SelectObject(m_gameBmp);

        //Первым делом плюхнем в него наш фон.

        m_gameDC.BitBlt(0,0,MAXX*PIXFORRECT,MAXY*PIXFORRECT,&m_dc,0,0,SRCCOPY);
       
        int x=0, y=0;
        // Затем в цикле будем проверять чего нам делать с полем.  
        for (x=0; x<MAXX; x++)
        {
                for (y=0; y<MAXY; y++)
                {
                        if (Place[x][y] == 1)
                                // Если на поле место занято положим в нужное место квадратик.
                                m_gameDC.BitBlt(x*PIXFORRECT,y*PIXFORRECT,x*PIXFORRECT+PIXFORRECT,y*PIXFORRECT+PIXFORRECT,&blockDC,0,0,SRCCOPY);
       
                        if (Place[x][y] == 2)
                                // См. Ниже.
                                m_gameDC.BitBlt(x*PIXFORRECT,y*PIXFORRECT,x*PIXFORRECT+PIXFORRECT,y*PIXFORRECT+PIXFORRECT,&fireDC,0,0,SRCCOPY);
                       
                }
        }
        // Теперь готовое поле заброси в объект с которого он будет браться для отображения на экране.
        m_showDC.BitBlt(0,0,MAXX*PIXFORRECT,MAXY*PIXFORRECT,&m_gameDC,0,0,SRCCOPY);
       
        // Уничтожим уже не нужный временный кадр.
        m_gameBmp.DeleteObject();
        m_gameDC.DeleteDC();
       
}
То что {Place[x([y] == 2} это условие, когда вся линия заполнена и подлежит удалению. Это будет динамически заполняться в алгоритме (помните о соглашениях) т.е. вместо отображения квадратика занятого поля мы рисуем там изображение огня, но вы можете сделать другой тип индикации, или даже не делать его вовсе...

3.3 Краткое резюме

Сделаем маленькое подведение итогов.
Мы имеем описанные данные и методику работы с ними
  • а) блоки
  • б) поле
  • в) работа с кадрами, формирующими наше отображение текущих данных на экран.
Стоит учесть, что по логике, так как мы отображаем поле, мы не озаботили себя отображением отдельного блока, сами движения и работа в игре будут происходить естественно в формализованном алгоритме. Графика посторена так, что бы отображать текущий статичный момент игры в каждом кадре и менять его при изменении положения в игре.


Автор: Гром
Версия для печати
Обсудить на форуме