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



4. Алгоритм.


Вернемся в начало и вспомним, какие типы действий у нас были.

Начнем работу с отработки действий, которые не зависят от пользователя и будут отрабатываться самостоятельно.
Возьмем наш объект ATetris и опишем его задачи.

а) старт игры.
б) выкладывание случайного нового элемента в стакан.
в) сдвиг текущего элемента вниз с проверкой на уже окончательное достижение позиции в стакане.
г) проверка на наличие заполненных линий в стакане и их стирание с последующим перемещением всего содержимого стакана.

Опишем старт игры.
Для нее нам понядобятся следующие данные, дабы оптимизировать процесс старта....
Добавим:
Код:
private:
bool IsStart; // флаг стартовала игра уже или нет.
int start_x,start_y; // стартовые значения для левого верхнего угла нового блока.
int current_position; // позиция текущего блока (степень его повернутости).

Если вы помните, у нас есть все 4 позиции каждого блока, изначально она нулевая и меняется в зависимости от количества нужных поворотов.
Код:
int current_block;   // собственно, номер текущего падающего блока в массиве ABlock объектов.
int LinesOut[MAXY];  // Массив для обнаруженных полных линий. Его необходимость станет понятна чуть позже.
int Error_subject;   // виды ошибок при выходе из рабочих процедур. Похоже на LastError в Windows
unsigned int Points; // Очки, набранные игроком
unsigned int Timeout; // текущий таймаут между шагами игры.
unsigned int Level;   // текущий уровень - изначально 0.

Добавим теперь две, первые после инициализации, функции
Код:
public:

void CreateBaseTetris(CDC * pDC);
void DestroyBaseTetris();

Первая, естественно, будет работать для создания базового поля, вторая - для его уничтожения...

Вот код...
Код:
void ATetris::CreateBaseTetris(CDC * pDC)
{
m_Place.CreateBasePlace(pDC);

}

void ATetris::DestroyBaseTetris()
{
m_Place.DestroyBasePlace();
}

Собственно, все. Ведь мы уже позаботились о том, что мы инициализируем и как, еще при работе с APlace. Теперь нам в алгоритме надо позаботиться о вызове методов, не более...

Очень нужная нам функция...
Код:
public:

CDC * GetTetrisDC(void);


CDC * ATetris::GetTetrisDC()
{
return m_Place.GetBaseDC();
}

Опять, как видите, запрос к APlace. Таким образом, работа объекта ATetris - служить фильтром между данными и программным интерфейсом, обеспечивая работу алгоритма.
Что, собственно, и требовалось доказать.

Если вы пойдете глубже, то поймете, что мы возвращаем m_showDC - т.е. сформированный к показу на экране кадр...

Далее опишем работу с приватными переменными класса:
Код:
public:
void SetNotStarted() {IsStart = false;}
void SetLevel(unsigned int lv) { Level = lv; }
bool IsStarted(void){ return IsStart;}
void SetStarted() {IsStart = true;}
int GetErrorSubject() { return Error_subject;}
unsigned int GetTimeOut() { return Timeout;}
void SetTimeOut(unsigned int to) {Timeout = to;};
void IncLevel(){Level++;};
unsigned int GetLevel(void){return Level;};
unsigned int GetPoints(){return Points;}
void SetPoints(unsigned int point) {Points = point;}

Это стандартные методы, которые нам понадобятся для работы с переменными класса игры - т.е. с данными уже самого алгоритма для пользователя...

Мы получили объект, который умеет проинициализировать данные и подготовить графику. Поставить в начальное состояние все рабочие переменные. О! Вот как раз этого мы пока не сделали...
Впишем в ATetris::ATetris(void) следующие строки:
Код:
IsStart = false;
Error_subject = 0;
Points = 0;
Timeout = STARTTIMEOUT;
Level = 0;

Теперь мы получили и стартовые значения. Кроме некоторых. Но надо еще дописать STARTTIMEOUT в ATetris.h
Код:
#define STARTTIMEOUT 1000 // стартовое значение таймаута
#define STOPTIMEOUT 10   // минимальное значение таймаута
#define POINTTOLINE 10   // количество очков за одну убранную строку
Все, кажется, теперь можно приступать к алгоритму непосредственно, и в частности, к первому пункту в списке - старту игры.

Для этого надо написать частный случай этого, добавление нового элемента.
Добавим в ATetris
Код:
bool SetNewBlock(int block_code);

bool ATetris::SetNewBlock(int block_code)
{

int x,y;
PTRIS_BLOCK tb;
int i,j;

Error_subject = 0;
current_block = block_code;

if (block_code < 0) block_code = 0;
if (block_code > 6) block_code = 6;
current_position = 0;

tb = pBlock[block_code]->GetBlock();

start_x = 2;
start_y = 0;


// здесь мы выполняем проверку - можем ли мы положить весь объект целиком
for(i=0; i<4; i++)
{
for (j=0; j<4; j++)
{
// if Place not 0
if (tb->block[current_position][i][j] + m_Place.GetPlace(j+start_x,i+start_y) > 1)
{
Error_subject = NEW_FULL;
return FALSE;

}
}
}


x=start_x;
y=start_y;

// собственно, установка элемента в массив поля.
for (int i=0; i<4; i++)
{
for (j=0; j<4; j++)
{
if (tb->block[current_position][i][j] == 1)
m_Place.SetPlace(x+j,y+i,tb->block[current_position][i][j]);
}
}
return TRUE;
}

Давайте немного поподробнее об алгоритме - хоть это и не цель и не лучший алгоритм, но все же.
Итак, при выполнении вложения нового элемента мы ничего не рисуем - как уже говорилось, мы формируем данные. В качестве данных у нас выступает стакан поля, которое надо заполнить. В частном случае начала игры никаких проверок не понадобится, но мы же пишем общий случай, поэтому первым делом после установки новых элементов на поле игры проверим, можем ли мы это сделать и не занято ли наше место на поле. Если занято, то мы уже проиграли.

Так как по договоренности у нас пустое значение ячейки поля равно нулю, а неиспользуемые места в блоке тоже равны нулю, то строка проверки выглядит так:
Код:
if (tb->block[current_position][i][j] + m_Place.GetPlace(j+start_x,i+start_y) > 1)
Это говорит о том, что любое значение одного из объектов сравнения, отличное от нуля, т.е. используемое или занятое, выдаст в сумме 2 или 3, если мы попытаемся сделать вложение в уже занятые места наш новый блок.

Сама установка массива в поле
Код:
for (int i=0; i<4; i++)
{
for (j=0; j<4; j++)
{
if (tb->block[current_position][i][j] == 1)
m_Place.SetPlace(x+j,y+i,tb->block[current_position][i][j]);
}
}

Это в уже проверенное место полю присваиваются значения блока из данной позиции... Позиция, как вы помните, это степень поворота блока.

Таким образом, иметь образ блока в объекте плюс готовое поле и значения текущей позиции блока на поле достаточно, чтобы отследить, как ведет себя блок в дальнейшем. Все достаточно просто, как видите.

Если вы попытаетесь откомпилировать приложение сейчас - у вас будет как минимум одна ошибка... Даже если все сделано правильно и ресурсы уже добавлены.

Это ошибка на неизвестном значении NEW_FULL. Это значение переменный ErrorSubjct которая служит для возрата значений, когда функция осуществляющая действие алгоритма это действие вполнить успешно не смогла. Все ее значения надо занести в ATetris.h
Код:
#define DOWN_LINE_OUT 1
#define DOWN_FULL 2
#define RIGHT_LINE_OUT 3
#define RIGHT_FULL 4
#define LEFT_LINE_OUT 5
#define LEFT_FULL 6
#define ROTATE_FULL 7
#define NEW_FULL 8
#define ERROR_OF_DATA 9

NEW_FULL - означает проигрыш игры, ведь мы заполнили стакан целиком. DOWN_FULL - будет говорить о необходимости посмотреть наличие полных линий и стереть их, а также выложить новый элемент. RIGHT_LINE_OUT - показывает, что мы, передвигая вправо падающий блок, достигли стенки станкана, и т.д.

И только одна ошибка, - это ошибка серьезная - которая у меня в игре пока не возникла, но обработать ее мы обязаны:
ERROR_OF_DATA - говорит об ошибке в используемых данных, а не в алгоритме. Например, неправильный номер поворота вне диапазона 0-3 или, что мы имеем отрицательный индекс в массиве поля.

Итак, новый элемент мы добавили, давайте пойдем дальше и начнем организовывать уже полный алгоритм с участием самой программы, так как мы вполне можем проверить нашу работу прямо сейчас.

4.1 Первый вид


Для работы с окном мы можем выбрать из двух классов, создаваемых Wizard-ом от VC++ - CMainFrm и CChildView. Естественно, что для отображения визуальной информации мы выберем второй, отвечающий за работу с визуальным отображением.

Давайте подключим уже имеющийся у нас код к нашей программе непосредственно.

В childview.h напишем:
Код:
#include "atetris.h"

private:
ATetris m_Tetr;

Сконcтруировав объект, проинициализируем наши данные...
В BOOL CChildView::PreCreateWindow(CREATESTRUCT& cs)  добавляем
Код:
if (!m_Tetr.Init()) {
AfxMessageBox("Init Memory error!");
return FALSE;

}

Для старта нашей игры создадим пункт в меню Start->New game и функцию-обработчик для него.
В этой функции выполним стартовые действия, которые положат старт нашей игре.
Код:
m_Tetr.SetPoints(0);
m_Tetr.SetLevel(0);

m_Tetr.CreateBaseTetris(this->GetDC());
m_Tetr.SetStarted();

Invalidate();

Т.е., разъясню. Изначально по старту программы будет вызвана функция Init, которая проинициализирует все объекты. Потом мы выполним дополнительную инициализацию уже для элементов, которые инициализируются не в старте программы, а при старте каждой игры: очки, уровень, флаг, стартовала ли игра, и создадим так называемый базовый DC, который будет готов к формированию кадров.

Затем в OnPaintDC() введем

Код:
pDC = m_Tetr.GetTetrisDC();
if (pDC)
dc.BitBlt(0,0,MAXX*PIXFORRECT,MAXY*PIXFORRECT,pDC,0,0,SRCCOPY);

Эти строки сделают следующее: мы вытащим из объекта уже готовый базовый кадр текущего положения и нарисуем его. Вернее, сделаем блит на текущий контекст.

Осталась самая малость...
Заставить эти самые кадры формироваться в зависимости от тайм-аута. Создадим обработчик сообщения WM_TIMER. А в объекте ATetris нам понадобится функция, которую я назвал Go. По причине того, что она, собственно, главная...
Код:
void Go();

void ATetris::Go()
{
m_Place.CreateGameDC();
}

Эта функция из той же прослойки, которая создается с помощью ATetris.

Впишем ее вызов в OnTimer() и сынициируем работу таймера в начале игры... SetTimer(1,1000,NULL) один раз в секунду.
Код:
OnTimer()
{
m_Tetr.Go();
Invalidate();

}

Вызов Invalidate() необходим для того, чтобы мы могли увидеть результаты работы приложения на экране.

Теперь, запустив приложение, мы можем видеть отображение на нем битмапки поля, которое отбражается в левом верхнем углу программы.

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