Вернемся в начало и вспомним, какие типы действий у нас были.
Начнем работу с отработки действий, которые не зависят от пользователя и будут отрабатываться самостоятельно.
Возьмем наш объект 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 или, что мы имеем отрицательный индекс в массиве поля.
Итак, новый элемент мы добавили, давайте пойдем дальше и начнем организовывать уже полный алгоритм с участием самой программы, так как мы вполне можем проверить нашу работу прямо сейчас.
Для работы с окном мы можем выбрать из двух классов, создаваемых 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() необходим для того, чтобы мы могли увидеть результаты работы приложения на экране.
Теперь, запустив приложение, мы можем видеть отображение на нем битмапки поля, которое отбражается в левом верхнем углу программы.
Гром