Данный материал взят с сайта old.shatalov.su и является его зеркалом

Создаём компьютерную игру. Создание игр на C++/DirectX

Есть вопросы?
Ошибка на сайте?
рус eng esp
Внимание! Данный сайт не обновляется. Новая версия: shatalov.su

Арканоид v0.2. Часть вторая

Дата создания: 2010-06-25 16:44:50
Последний раз редактировалось: 2012-03-01 12:03:34

Продолжаем рассмотрение арканоида. Перед чтением второй части обязательно необходимо ознакомиться с первой.

Вторая версия была написана где-то за двадцать часов - полные выходные. Сам игровой процесс был полностью скопирован из версии 0.1, которую мы обсуждали в прошлом уроке.

В программе используется музыкальное сопровождение. Для вывода звука я использовал XAudio2. К сожалению, это слишком большая тема, чтобы даже кратко затронуть её в сегодняшнем выпуске. О звуке мы будем говорить гораздо позже.

Код я вам не дам. Вы должны написать его сами. Картинки для интерфейса находятся в папке с игрой.

Игровой интерфейс

Интерфейс 0.2 версии основывается на том, что был в программе Клетки v0.3. Но при этом есть небольшие улучшения. Давайте взглянем на метод рисования элемента интерфейса:

void DrawIntObject(D3DLOCKED_RECT& lockedRect)
{
  const int INACTIVE = 0x00999999;
  for (int i = 0; i < height; ++i)
    for (int j = 0; j < width; ++j)
    {
      if (img[j+i*width] != 0x00ffffff)
        memcpy(reinterpret_cast(lockedRect.pBits)
               +x*4+j*4+i*lockedRect.Pitch+y*lockedRect.Pitch,
               reinterpret_cast(&img[j+i*width]),4);
      if (active == 0 && img[j+i*width] == 0x00ccffff)
        memcpy(reinterpret_cast(lockedRect.pBits)
               +x*4+j*4+i*lockedRect.Pitch+y*lockedRect.Pitch,
               &INACTIVE,4);
    }
}

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

В данном коде я добавил выделение активности/неактивности элемента. Если на картинке есть пиксели цвета 0x00ccffff (смотрите текст на картинках в папке images), то, если элемент неактивен, они выводятся более тёмным цветом - 0x00999999. Больше никаких изменений в класс вносить не нужно. Просто в графическом редакторе нужно рисовать элементы интерфейса с добавлением цвета 0x00ccffff. Такой нехитрый приём существенно повысил дружелюбность нашего интерфейса.

В программе 41 элемент интерфейса. Большую часть написания программы вы потратите как раз на заполнение интерфейса:

enum Controls{SKIN,   // основной интерфейс
              MENU_BUTTON,
              EDITOR, // редактор
              SAVE_LEVEL,
              LEVEL1,
              LEVEL2,
              LEVEL3,
              LEVEL4,
              LEVEL5,
              LEVEL6,
              LEVEL7,
              LEVEL8,
              LEVEL9,
              LEVEL0,

              LESS_XGAP,
              MORE_XGAP,
              LESS_YGAP,
              MORE_YGAP,
              LESS_OX,
              MORE_OX,
              LESS_OY,
              MORE_OY,
              MORE_COLS,
              LESS_COLS,
              MORE_ROWS,
              LESS_ROWS,

              TILE_TYPE_NONE,
              TILE_TYPE_SOLID,
              TILE_TYPE_GREEN,
              TILE_TYPE_ORANGE,
              ARROW,

              MENU,  // меню
              NEW_GAME,
              RETURN,
              SAVE,
              LOAD,
              EDITOR_BUTTON,
              EXIT,
              END_GAME,
              END_GAME_MENU,
              MUSIC};

У большинства элементов родительскими являются или EDITOR или MENU. Некоторые элементы стоят особняком - END_GAME. Когда вы начнёте строить интерфейс, вам станет понятно, какой элемент куда поместить.

Элементы хранятся в массиве указателей (я взял с запасом 50):

const int NUM_INT_ELEMENTS = 50;
IntObject* intTable[NUM_INT_ELEMENTS];

Сама таблица заполняется в функции InitGraphics. Функция ничем кроме количества элементов не отличается от той, что была в клетках.

После загрузки элементов интерфейса происходит загрузка графики для мяча и блоков (для платформы используется сплошной чёрный цвет). Вот как выглядит загрузка картинки одного из блоков:

std::ifstream is4 ("images/game/metal.bmp",std::ios::binary); is4.seekg(54,0); for (int i = 19; i >= 0; --i) is4.read(reinterpret_cast(&mdTiles[2][i*50]),50*4); is4.close();

Картинка считывается построчно (размер блока - 20*50).

Пусть вас не смущает название массива - mdTiles. Просто сначала я планировал добавить возможность изменения размера блоков. md - от middle (средний). Но времени было мало и от этой идеи пришлось отказаться. В правом нижнем углу редактора, на месте элементов интерфейса, с помощью которых планировалось менять размер блоков, сейчас расположена надпись Арканоид v0.2.

Интерфейс обрабатывается также как и в клетках - через функции CheckInt и IntProc. В данном случае IntProc очень длинная функция.

Самое сложное - обработать элементы в редакторе. Там возможны разные ситуации. Например, если выбирается один из уровней, он должен стать неактивным, а все остальные активными. В этот момент также нужно проверить активность элементов центральной группы (различные отступы) - ведь блоки данного уровня могут находиться вверху экрана, а значит, элемент двигающий блоки вверх должен быть неактивным.

Для проверки всех элементов интерфейса я использовал вспомогательную функцию CheckInterface. Она вызывается при нажатии почти на любом элементе интерфейса левой и центральной группы элементов в редакторе. Функция проверяет переменные текущего уровня и устанавливает активность элементов интерфейса зависящих от этой переменной. Давайте посмотрим на парочку примеров:

case MORE_ROWS:
  intTable[LESS_ROWS]->active = 1;
  lvlTable[currentLevel]->ResizeLevel(lvlTable[currentLevel]->rows+1,
  lvlTable[currentLevel]->columns);
  intTable[SAVE_LEVEL]->active = 1;
  CheckInterface();
  break;

Здесь пользователь нажимает кнопку, увеличивающую количество строк (more - больше). Мы сразу можем сделать активной кнопку, уменьшающую количество строк. Вторую строку пока трогать не будем - здесь изменяется размер уровня. После этого элемент SAVE_LEVEL можно сделать активным - уровень можно сохранять, только когда в нём были сделаны хоть какие-нибудь изменения. В конце вызывается функция CheckInterface. Посмотрим на её кусок:

intTable[MORE_OX]->active = 1;
intTable[LESS_OX]->active = 1;
intTable[MORE_OX]->active = 1;
intTable[MORE_OY]->active = 1;
intTable[LESS_OY]->active = 1;
intTable[MORE_XGAP]->active = 1;
intTable[LESS_XGAP]->active = 1;
intTable[MORE_YGAP]->active = 1;
intTable[LESS_YGAP]->active = 1;
intTable[MORE_COLS]->active = 1;
intTable[LESS_COLS]->active = 1;
intTable[MORE_ROWS]->active = 1;
intTable[LESS_ROWS]->active = 1;

if (lvlTable[currentLevel]->ox == 1)
{
  intTable[LESS_OX]->active = 0;
}
if (lvlTable[currentLevel]->ox == 50 ||
    c->ox+c->columns*(c->blockWidth+c->xgap)-c->xgap >= 499)
{
  intTable[MORE_OX]->active = 0;
}

if (lvlTable[currentLevel]->oy == 25)
{
  intTable[LESS_OY]->active = 0;
}
if (lvlTable[currentLevel]->oy == 75 ||
    c->oy+c->rows*(c->blockHeight+c->ygap)-c->ygap >= 349)
{
  intTable[MORE_OY]->active = 0;
}

Все элементы центральной группы редактора сначала нужно сделать активными. Затем проверяются переменные текущего уровня (current - текущий).

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

Обратите внимание, как считается правая граница последнего блока: c->ox+c->columns*(c->blockWidth+c- >xgap)-c->xgap. Подобных вычислений в этой функции много. Такие вычисления нужны, чтобы при увеличении отступа, количества блоков, расстояния между блоками, ничего не вылезло за экран. Приятного программирования!

Надеюсь, назначение функции CheckInterface понятно. Она используется только чтобы определить активность центральных элементов в редакторе для текущего уровня. Возвращаемся к IntProc. Давайте посмотрим, как загружается третий уровень:

case LEVEL3:
  intTable[SAVE_LEVEL]->active = 0;
  for (int i = LEVEL1; i <= LEVEL0; ++i)
    intTable[i]->active = 1;
  intTable[LEVEL3]->active = 0;
  lvlTable[currentLevel]->ReloadLevel();
  currentLevel = 2;
  CheckInterface();
  break;

Самое важное - две строки перед вызовом CheckInterface. Перед тем как поменяется текущий уровень, он перезагружается (reload). Каждый раз, когда пользователь нажимает "Сохранить уровень", этот уровень записывается в файл. При перезагрузке все изменения, сделанные после последнего сохранения, теряются. В ReloadLevel происходит загрузка уровня из соответствующего ему файла.

Вывод блоков на экран

Вывод блоков в процессе игры и в редакторе немного отличается. Для пустого блока в редакторе выводится рамка толщиной в один пиксель. Подобные рамки мы рисовали в программе клетки.

Игровая логика

Во время игры конечный автомат может находиться в одном из трёх состояний: NEW_GAME_STATE, START_LEVEL_STATE, RUN_STATE. Всего в программе шесть состояний:

enum gameState {NEW_GAME_STATE,
                RUN_STATE,
                MENU_STATE,
                EDITOR_STATE,
                START_LEVEL_STATE,
                END_GAME_STATE};
gameState fsm = MENU_STATE;

Когда пользователь нажимает "Новая игра", конечный автомат переходит в NEW_GAME_STATE:

if (fsm == NEW_GAME_STATE)
{
  currentLevel = 0;
  scores = 0;
  lives = 3;
  lvl = 1;
  lvlText[8] = lvl+48;
  fsm = START_LEVEL_STATE;
  deadFlag = 0;
}

Здесь устанавливается текущий уровень, очки, текст, отображаемый вверху, флаг поражения. Игра переходит в состояние START_LEVEL_STATE. Код для состояния START_LEVEL_STATE намного сложнее, чем в предыдущей версии:

if (fsm == START_LEVEL_STATE)
{
  paddle_x = PADDLE_START_X;
  paddle_y = 499 - 25;
  ball_x = paddle_x+PADDLE_WIDTH/2-4;
  ball_y = paddle_y-BALL_SIZE;
  ball_dx = 0;
  ball_dy = 0;

  if (currentLevel == 10)
  {
    lvl--;
    if (lvl != 10)
    lvlText[8] = lvl+48;
    else
    {
      lvlText[8] = 1+48;
      lvlText[9] = 0+48;
    }
    fsm = END_GAME_STATE;
    intTable[END_GAME]->visible = 1;
    intTable[END_GAME_MENU]->visible = 1;
    intTable[END_GAME_MENU]->active = 1;
    intTable[MENU_BUTTON]->active = 0;
    return;
  }
  if (deadFlag == 0)
    lvlTable[currentLevel]->ReloadLevel();
  else
    deadFlag = 0;

  int notHollow = 0;
  for (int i = 0; i < lvlTable[currentLevel]->rows*lvlTable[currentLevel]->columns;++i)
  {
    if (lvlTable[currentLevel]->map[i] > 0)
    {
      notHollow = 1;
      break;
    }
  }
  if (notHollow == 0)
  {
    ++currentLevel;
    return;
  }
  fsm = RUN_STATE;
  levelLoaded = 0;
  return;
}

Здесь можно выделить три части. В первой происходит установка платформы и мяча. Вторая и третья части гораздо интересней.

Когда вы играете в арканоид, вверху экрана отображается текущий уровень. currentLevel - это переменная, которая хранит номер уровня. Это служебная переменная, с помощью неё отслеживается текущий уровень как в игре, так и в редакторе. Также с помощью неё происходит загрузка и сохранение текущего уровня. lvl - это число, которое пользователь видит вверху экрана. currentLevel и lvl не всегда совпадают: currentLevel начинается с нуля, а lvl - с единицы.

Во второй части кода обрабатывается значение переменной currentLevel равное 10. Это значение переменная получает только в одном случае - в конце игры, когда были пройдены уровни от нуля до 9 (эти уровни могут быть пустыми). Если lvl не равно 10 (в игре были пустые уровни), то в текстовую строку, которая выводится вверху экрана, заносится однозначное число. Если lvl равно 10 (были сыграны все десять уровней), то в строку заносится двузначное число - десять.

После этого делается видимым финальное меню.

Давайте ещё раз кратко: lvl нужна, чтобы правильно перечислить все уровни в игре, даже если некоторые уровни отсутствовали. Например, пользователь создал в редакторе уровни с номерами 0, 5, 9. Эти значения во время игры будет принимать currentLevel. А lvl будет принимать значения 1, 2, 3. Надеюсь, понятно.

Переходим к коду, следующему за проверкой на финальный уровень. Напоминаю, что мы находимся в состоянии начала нового уровня (START_LEVEL_STATE). Здесь нужно проверить флажок смерти. Если в данное состояние игрок попал с активным флажком, то нужно продолжать текущий уровень (игрок пропустил мяч - минус одна жизнь). Если флажок смерти не активен (это значит, что игрок прошёл предыдущий уровень), то мы перезагружаем уровень.

В последней части проверяется флаг notHollow (hollow - пустой). Здесь проверяется, есть ли в уровне хотя бы один блок. Если в уровне блоков нет, то переходим (currentLevel) на следующий уровень (lvl не меняется).

И в конце переходим к состоянию RUN_STATE. Остался один флажок - levelLoaded. Во время игры он должен быть равен нулю. Этот флажок устанавливается при загрузке игры с диска. Как он работает, чуть ниже.

Последнее состояние - RUN_STATE. Код здесь очень похож на тот, который был использован в версии 0.1. Я обозначу лишь отличия:

В самом начале:

if (levelLoaded == 1)
  fsm = START_LEVEL_STATE;

Во время игры можно выйти в меню. Когда пользователь набрал хоть немного очков, кнопка "Продолжить" становится активной. "Новая игра" переходит в состояние GAME_INIT_STATE, а "Продолжить" - в RUN_STATE. Когда пользователь выходит в меню, он может нажать на кнопку "Загрузить". В это время устанавливается флажок levelLoaded. Когда пользователь нажимает "Продолжить", то этот флажок говорит, что надо проинициализировать currentLevel уровень заново. Заодно платформа и мяч переместятся в исходное положение.

Следующий интересный нам кусок кода расположен в проверке вылета мяча за платформу:

if (ball_y > 500-15)
{
  deadFlag = 1;
  lives -= 1;
  if (lives < 0)
  {
    fsm = END_GAME_STATE;
    intTable[END_GAME]->visible = 1;
    intTable[END_GAME_MENU]->visible = 1;
    intTable[END_GAME_MENU]->active = 1;
    intTable[MENU_BUTTON]->active = 0;
  }
  else
    fsm = START_LEVEL_STATE;
}

Здесь устанавливается флажок смерти. Далее уменьшается количество жизней. Теперь нужно проверить, не кончились ли жизни? Если кончились, то показываем финальное окно. Если не кончились, то переходим к START_LEVEL_STATE. Напоминаю, что при установленном deadFlag уровень не перезагружается (это очень важно - пользователю не нужно начинать сначала).

Теперь нужно проверить окончание уровня. Если на поле остались только пустые и твёрдые (которые нельзя разбить) блоки, то уровень кончился.

int levelEnded;
levelEnded = 1;
for (int i = 0; i < lvlTable[currentLevel]->rows*lvlTable[currentLevel]->columns;++i)
{
  if (lvlTable[currentLevel]->map[i] > 0)
  {
    levelEnded = 0;
    break;
  }
  levelEnded = 1;
}

При окончании уровня нужно сделать несколько вещей:

if (levelEnded == 1)
{
  ++currentLevel;
  fsm = START_LEVEL_STATE;
  lvl++;
  if (lvl != 10)
    lvlText[8] = lvl+48;
  else
  {
    lvlText[8] = 1+48;
    lvlText[9] = 0+48;
  }
  return;
}

Сначала увеличивается currentLevel. Затем меняется состояние конечного автомата на начало уровня. Увеличивается lvl. Здесь же второй раз проверяется однозначность/двузначность числа lvl - дошёл ли пользователь до десятого уровня.

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

Меню

По меню хотелось бы сделать несколько замечаний:

При входе в редактор весь прогресс достигнутый игроком теряется. У меня просто обнуляются очки (scores), а именно эта переменная включает кнопку "Продолжить". Кнопка "Сохранить" становится доступной только после того, как пользователь прошёл хотя бы один уровень (проверяется с помощью lvl). "Загрузить" становится доступной, только когда в файле save.sav хоть что-то есть.

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

Уровни

Наконец-то мы подошли к части программы, которую мне бы хотелось выделить особо.

В игре доступно десять уровней. Все уровни лежат в папке levels. Файл levels в этой папке - это сохранённый уровень. Вначале я хотел, чтобы была возможность давать имена файлам, но тогда возникает ряд вопросов, разъяснение которых потребовало бы выпуска два. Поэтому я использовал заданные имена.

Давайте посмотрим на класс уровней:

class Level
{
public:
  char name[14];    // имя файла
  bool exist;       // существует ли уровень
  int columns;      // количество столбцов
  int rows;         // количество строк
  int ox,oy;        // отступы левого верхнего блока от начала экрана
  int xgap,ygap;    // расстояния между блоками
  int blockSize;    // размер блока
  int blockWidth;   // ширина блока
  int blockHeight;  // высота блока

  char* map;        // блоки
};
Методы обсудим чуть ниже.
При запуске игры нужно загрузить все уровни в массив указателей lvlTable. 
char name[] = "levels/level0";
for (int i = 0; i < 10; ++i)
{
  name[12] = i+48;
  lvlTable[i] = new Level(name);
}

Здесь меняется имя (последний символ в имени - число). И для каждого имени вызывается конструктор. При загрузке файла нужно учесть два случая: в файле может находиться уровень, и файл может быть пустым. В первом случае мы вытаскиваем все переменные из файла. Порядок переменных можете определить сами. Главное: последним должен идти массив map. Во втором случае мы создаём уровень на лету (я создаю поле блоков размером 5*5 с пустыми блоками). Вот код конструктора:

Level(char fname[14])
{
  for (int i = 0; i < 14; ++i)
    name[i] = fname[i];
  exist = 0;
  std::ifstream is (name,std::ios::binary);
  is.read(reinterpret_cast(&columns),sizeof(columns));
  if (is) // уровень существует
  {
    is.read(reinterpret_cast(&rows),sizeof(rows));
    is.read(reinterpret_cast(&ox),sizeof(ox));
    is.read(reinterpret_cast(&oy),sizeof(oy));
    is.read(reinterpret_cast(&xgap),sizeof(xgap));
    is.read(reinterpret_cast(&ygap),sizeof(ygap));
    is.read(reinterpret_cast(&blockSize),sizeof(blockSize));
    is.read(reinterpret_cast(&blockWidth),sizeof(blockHeight));
    is.read(reinterpret_cast(&blockHeight),sizeof(blockWidth));

    map = new char[columns*rows];
    is.read(map,rows*columns);
    is.close();
    exist = 1;
  }
  else // файл c именем name оказался пустым
  {
    ox = 10;
    oy = 35;
    blockSize = 1;
    xgap = 10;
    ygap = 10;
    columns = 5;
    rows = 5;
    blockWidth = 50;
    blockHeight = 20;
    map = new char[columns*rows];
    for (int i = 0; i < columns*rows; ++i)
      map[i] = -1;
  }
}

По-моему у меня здесь лишняя переменная exist (существует). Я не смог обнаружить её использования в других местах программы (может быть, в редакторе используется, не помню). blockSize - точно лишняя переменная. Данная переменная определяла размер блока (сначала я хотел сделать возможность изменения размера блоков). Её заменяют переменные width и height.

В конструкторе можно увидеть, как определяется существование файла.

В деструкторе нужно обязательно освободить память от map.

Метод ReloadLevel вызывается, чтобы загрузить уровень из файла заново. Например, он вызывается? если пользователь не сохранил уровень в редакторе и вышел оттуда. Сам метод очень похож на конструктор. Тоже нужно учесть два случая.

Метод SaveLevel сохраняет уровень на диск. У меня он перегружен два раза. Без аргументов этот уровень сохранятся под именем своего поля name. Этот случай используется при сохранении уровня в редакторе. Второй случай - когда пользователь сохраняет игру. Здесь в метод передаётся имя файла (всегда levels).

Метод LoadLevel предназначен для загрузки уровня. В использовании немного отличается от ReloadLevel. LoadLevel используется для загрузки игры, которую пользователь сохранил ранее. В метод передаётся имя файла (всегда levels).

Метод ResizeLevel предназначен для изменения размера уровня (вызывается при изменении map). В данном случае создаётся временный массив, в который сохраняется map. map удаляется. Затем выделяется память для map нового размера и в map копируется содержимое временного.

Здесь есть один важный момент. map - одномерный массив. Возникает вопрос: как удалить правый столбец? Я рекомендую написать этот метод самостоятельно. Чрезвычайно важно понимать, как это работает. На всякий случай приведу полный код. Аргументами передаётся новое количество строк и столбцов:

void ResizeLevel(int r,int c)
{
  char* t = new char[columns*rows];
  for (int i = 0; i < rows; ++i)
    for (int j = 0; j < columns; ++j)
      t[j+i*columns] = map[j+i*columns];

  delete [] map;
  map = NULL;
  map = new char[c*r];

  for (int i = 0; i < r; ++i)
    for (int j = 0; j < c; ++j)
      map[j + i*c] = -1;   // -1 - пустой блок

  if (r <= rows)           // строк стало меньше
  {
    if (c <= columns)      // столбцов стало меньше
      for (int i = 0; i < r; ++i)
        for (int j = 0; j < c; ++j)
          map[j + i*c] = t[j+i*columns];
    else if (c > columns)  // столбцов стало больше
      for (int i = 0; i < r; ++i)
        for (int j = 0; j < columns; ++j)
          map[j + i*c] = t[j+i*columns];
  }
  else if (r > rows) // строк стало больше
  {
    if (c <= columns)
      for (int i = 0; i < rows; ++i)
        for (int j = 0; j < c; ++j)
          map[j + i*c] = t[j+i*columns];
    else if (c > columns)
      for (int i = 0; i < rows; ++i)
        for (int j = 0; j < columns; ++j)
          map[j + i*c] = t[j+i*columns];
  }

  rows = r;
  columns = c;
  delete [] t;
}

Правильное добавление и удаление столбцов показано в проверках c <= columns и c > columns.

В программе есть ещё две функции (не метода): SaveLevel и LoadLevel. В этих функциях вызываются соответствующие методы текущего уровня. Помимо этого в файл save.sav сохраняется (или загружается из него) информация о текущей игре: количество жизней, текущий уровень (и lvl, и currentLevel), количество очков.

По программе всё.

Столкновение двух прямоугольников

Пора добавить в наши программы новый тест - столкновение двух прямоугольников. Это сделает наши программы чуть-чуть более реалистичными. Столкновение двух прямоугольников гораздо более сложный случай, чем столкновение точки и прямоугольника. Способов осуществить такую проверку довольно много. Я покажу самый простой.

Сначала давайте разберёмся, как столкновение двух прямоугольников выглядит на одной оси - пересечение двух отрезков:

1  xxxx
2    xxxx
#--------->x
0123456789

Координаты этих отрезков выглядят так: 1(3,6), 2(5,8). Прошлый выпуск рассылки показал, что иногда для упрощения уравнений нужно использовать не сами координаты, а минимальные и максимальные значения. Минимумы и максимумы этих отрезков будут выглядеть так:

min1 = 3;
max1 = 6;
min2 = 5;
max2 = 8;

Тест на столкновение двух отрезков очень прост: максимум первого отрезка должен быть больше минимума второго и максимум второго - больше минимума первого:

if (max1 >= min2 && max2 >= min1)
{
  // отрезки пересекаются
}

Обратите внимание, что этот тест работает и вот в таком случае (благодаря тому, что мы используем min и max):

1    xxxx
2 xxxx
#--------->x
0123456789

Теперь можно перенести этот тест в двухмерное пространство. Достаточно добавить такую же проверку для второй оси:

if ( xmax1 >= xmin2 && xmax2 >= xmin1 &&
     ymax1 >= ymin2 && ymax2 >= ymin1 )

Осталось только написать простейшую функцию определения минимума и максимума из двух чисел.

Улучшение интерфейса

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

Сейчас элемент считается нажатым, если был пройден тест:

if (x > intTable[i]->x &&
    x < intTable[i]->x+intTable[i]->width &&
    y > intTable[i]->y &&
    y < intTable[i]->y+intTable[i]->height)
  IntProc(i); 

Т.е. если курсор был нажат над прямоугольником, который обозначает данный элемент. Мы можем очень легко добавить в наш интерфейс элементы любой формы. Для этого достаточно после вышеприведённой проверки сделать ещё одну - на какой пиксель попал курсор: если на прозрачный (белый), то элемент не нажат. Это может выглядеть так:

bool IntObject->CheckCursor (int x, int y)
{
  if (img[x+y*width] == 0x00ffffff)
    return 0;
  return 1;
}

Тогда весь тест будет выглядеть так:

if (x > intTable[i]->x &&
    x < intTable[i]->x+intTable[i]->width &&
    y > intTable[i]->y &&
    y < intTable[i]->y+intTable[i]->height)
  if (intTable[i]->CheckCursor(x,y))
    IntProc(i);

Теперь можно создавать элементы любой формы. Достаточно только неактивные зоны обозначить белым цветом.

Заключение

Арканоид v0.2 является без сомнения знаковой для нас программой. Во-первых, программа сама по себе очень длинная (у меня больше двух тысяч строк, у вас будет поменьше). Во-вторых, здесь встречается много новых для нас вещей: система уровней, редактор, движение объектов.

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

Упражнения

1. Полностью арканоид можете не делать. Но обязательно реализуйте следующие возможности: систему уровней и редактор. Это самое важное в программе. Будет непросто, но пересильте себя и сделайте. По времени создание программы займёт часов двадцать. На сегодня всё. До скорой, надеюсь, встречи.