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

Предисловие

В этой части советов я решил оформить серию статей по работе с графикой в окнах Windows без использования работы DirectX и OpenGL, а только посредством стандартных функций API и MFC.
Кроме того, важно понять после советов в виде отрывочных статей, как правильно планировать и писать программы под Windows с учетом всех ее особенностей.
Для хорошего понимания, как всегда, я использую работу на примере. А писать в этот раз мы будем Тетрис, игрушку вечную, но до сих пор популярную.

1. Планирование данных.

В первую очередь программное планирование начинается с определения и формализации задачи. Саму задачку мы определили, как Тетрис обыкновенный, и теперь понимая нашу цель можем приступить к формализации задачи.
Разделим программму на данные и методы работы с ними. В качестве данных будут выступать два основных объекта игры - падающий блок и игровое поле.
Создадим обычную апликацию, которая в Wizard-е от студии называется MFC Application. Обзовем ее tris. В окне Application type выберем Single Document, а птичку поддержки структуры Document/View выключим, так как работа с документом в его стандартном виде нам не пригодится.
В результате мы будем иметь простое окошко с менюшкой и белым фоном, на котором ничего нет...
Первым объектом данных будет класс ABlock.
Определим тип данных которым этот объект будет владеть и с которым мы будем работать.
Код: (C++)
typedef struct _TRIS_BLOCK
{
        int block[4][4][4];
} TRIS_BLOCK, * PTRIS_BLOCK;
Можно было бы обойтись и просто описанием массива. Но так получается более наглядно, к тому же в будущем нам может понадобиться расширить свойства блока, добавив, например, цвет.
Вы спросите почему 3 уровня массива. И я вам отвечу - мы имеем дело с выбором между избыточностью данных и избыточностью (сложностью) кода.
Каждый блок падая, поворачивается на 90 градусов. Мы имеем выбор - либо взять и написать алгоритм поворота объекта, либо держать в данных все 4 его положения, при повороте манипулируя его индексами в массиве.  Я выбрал второй путь, так как алгоритм поворота мне писать лень :)
Сам класс блок получился очень простым:
Код: (C++)
class ABlock
{
public:
       
        ABlock(TRIS_BLOCK ptb);
        ~ABlock(void);
        PTRIS_BLOCK GetBlock();
       

private:
        TRIS_BLOCK tb;

};
В файле ABlock.cpp пропишем содержимое 2-х функций...
Код:
ABlock::ABlock(TRIS_BLOCK in_tb)
{
memset(&tb,0,sizeof(int) * 4*4*4);
memcpy(&tb,&in_tb,sizeof(int) * 4*4*4);
}

PTRIS_BLOCK ABlock::GetBlock()
{
return &tb;
}
Обратите внимание на то, что объект не имеет пустого конструктора типа ABlock(), потому что без данных сам по себе объект нам никогда не понадобится.
На этом объект Block мы закончили и приступим к второму типу данных - поле (Place).
Создадим такой - же пока пустой объект APlace, и опишем три константы:
Код: (C++)
#define MAXX    10  // Ширина поля
#define MAXY    20  // Высота поля

#define PIXFORRECT      20 // Колличество точек на квадрат(ячейку) (в данном случае квадрат - это один элемент стакана тетриса, где может располагаться честь блока)
Значения для поля изначально установим равными нулю, что означает пустой квадрат.
Код: (C++)
class APlace
{
public:
        APlace(void);
        ~APlace(void);
};
В этом объекте нам понадобятся данные - само поле. Представим поле в виде двухмерного массива ячеек:
Код: (C++)
private:
        int Place[MAXX][MAXY];
Естественно, что в данном случае за ячейку поля мы определяем 1 квадрат.
В секцию public запишем:
Код: (C++)
int SetPlace(int x, int y, int set_point);
int GetPlace(int x, int y);
Эти функции будут использоваться для управления значениями ячеек поля. И заполним наш APlace.cpp:
Код:
APlace::APlace(void)
{
memset(Place,0,sizeof(int)*MAXX*MAXY);
}

APlace::~APlace(void)
{

}

int APlace::SetPlace(int x, int y, int set_point)
{

if (x > MAXX) return 0xFFFF;
if (y > MAXY) return 0xFFFF;
if ((x<0) || (y<0)) return 0xFFFF;
Place[x][y] = set_point;

return 1;
}

int APlace::GetPlace(int x,int y)
{
if ( (x > MAXX) || (x < 0) || (y>MAXY) || (y <0) ) return 0xFFFF;
return Place[x][y];
}
Мы обеспечили минимально необходимую организацию данных, и можем перейти к разработке логики игры - т.е. непосредственно к алгоритму. Все дунные описаны и формализованы.

2. Разработка алгоритма

Прежде, чем перейти к алгоритму, я бы хотел напомнить, или показать, тем кто не знает, основные принципы работы окна программы на программном уровне.
Как я уже писал, программа на Windows - это обычный цикл, ждущий прихода сообщений и на них реагирующий. На деле это обычный вызов функции по появлении в очереди сообщений, данных, предназначенных текущему окну.
Вызов и отработка сообщений происходит в функции DispatchMessage();
В MFC основную работу по созданию такого цикла код программы скрывает - оставляя нам макросы ON_COMMAND и ON_MESSAGE в которых с помощью wizard-а мы можем создавать функции обработчики таких сообщений. В цель этой статьи не входит рассказ где найти в студии той или иной версии VC++ те или иные сообщения.
Для отработки алгоритма, всегда приходится делить действия, на происходящие по велению пользователя: работа с меню, или кнопками, запрос на установку и вычитывание данных, работа с клаиатурой и мышкой; и на те, которые программа должна делать независимо от пользователя, без его требования.
В нашем случае Тетрис постоянно в момент работы делает следующие процедуры:
  • выставляет новый блок в верх поля, если текущий блок достиг низа, или это старт программы.
  • сдвигает текущий блок вниз с определенной скоростью.
  • проверяет на наличие заполненных строк в стакане (поле) для их уничтожения и сдвигает оставшиеся выше части вниз.
  • в случае невозможности выставить новый блок (стакан заполнен) принимает решение о окончании игры.

Все эти действия необходимо делать постоянно. Есть для этого несколько способов.
  • в API структуре можно упихать всех их в постоянный цикл проверки сообщений однако, такой метод не рекомендуется, так как в более насыщенных действиями программах это будет сильно тормозить обработку сообщений.
  • созданием дополнительного потока (thread) в котором будут происходить все действия. Чаще применяется для работы с программмами требующими постоянной работы и слежения за потоками данных.
  • если действия дискретны и происходят 1 раз за постоянный промежуток времени, то всех их "сажают" на сообщение таймера (WM_TIMER).

У нас действия более чем подходят под пункт "c" в котором каждое действие будет происходить по тайм-ауту, который в зависимости от сложности уровня можно уменьшать.
Логика самой игры подсказывает следующее решение. Физически наши данные будут представлять из себя набор значений.
Для блока при максимальной длине элемента в четыре ячейки (прямая линия) мы определили в качестве данных массив, который содержит матрицу самого элемента с значениями;
1 - блок в ячейке присутствует.
0 - ячейка блоком не используется.
массив таких матриц из 4-х положений поворота составит все возможные положения блока в пространстве поля.
Поле, представляющее из себя двухмерную матрицу будет получать отображение текущей матрицы элемента в виде заполненных ячеек теми же единицами. При заполнении есть возможность проверить мешает ли что-либо установить блок в текущую позицию сравнивая значения из используемых блоком ячеек с соответствующими им ячейками поля.
При этом ячейки поля смогут принимать не два, а более значений, которые могут сказать нам о том, что данная ячейка принадлежит линии поля полностью заполненной, которая подлежит уничтожению, или даже о цвете данной ячейки, буде появиться такая необходимость...
В любом случае, так мы универсально сможем закодировать состояние игры которое сможем отображать на экране.
После того как принцип алгоритма (шаговый и по возбуждению пользователем), а так же принцип формальной записи процесса в данные стал понятен приступим к разработке самого игрового интерфейса.
Версия для печати
Обсудить на форуме