NeHe Tutorials Народный учебник по OpenGL
Урок 32. OpenGL

Picking, Alpha Blending, Alpha Testing, Sorting

 

Добро пожаловать на тридцать второй урок. Это, пожалуй, самый большой урок, который я написал. Более чем 1000 строк кода, и более чем 1540 строк текста. Это также первый урок, в котором использован новый базовый код "NeHeGL basecode". Этот урок отнял у меня много времени, но я думаю, он стоит этого. Вот некоторые из тем, которые рассматриваются в этом уроке: альфа смешивание, альфа тест, получение сообщений от мыши, одновременное использование ортографической и перспективной проекций, отображение собственного курсора, ручная сортировка объектов с учетом глубины, хранение кадров анимации в одной текстуре и, что, пожалуй, самое важное Вы овладеете ВЫБОРОМ (picking)!

В первоначальной версии этого урока на экране отображались три объекта, которые изменяли цвет, когда на них нажимали. Вам это интересно!?! Не возбуждает вообще! Как всегда, мои дорогие, я хотел впечатлить Вас крутым уроком высшего качества. Я хотел, чтобы урок вам понравился, набил ваши мозги ценной информацией и конечно... клево выглядел. Итак, после нескольких недель кодирования, урок сделан! Даже, если Вы не программируете, Вы можете просто наслаждаться результатом этого урока. Это полноценная игра! Вы будете стрелять по множеству целей, до тех пор, пока ваш боевой дух (morale) не упадет ниже предельной черты или ваши руки сведёт судорогой, и Вы больше не сможете щелкать по кнопке мыши.

Я уверен, что в мой адрес по поводу этого урока будет критика, но я очень счастлив, что создал этот урок! Такие малоприятные темы, как выбор и сортировка объектов по глубине, я превратил в забавные!

Несколько небольших замечаний о коде, я буду обсуждать только ту часть кода, которая находится в файле lesson32.cpp. Произошло несколько незначительных изменений в коде NeHeGL. Наиболее важное изменение то, что я добавил поддержку мыши в WindowProc(). Я также добавил int mouse_x, mouse_y, чтобы сохранить положение курсора мыши. В NeHeGL.h две следующие строки кода были добавлены: extern int mouse_x; & extern int mouse_y;.

Все текстуры, которые используются в этом уроке, были сделаны в Adobe Photoshop. Каждый TGA файл – это 32-битное изображение с альфа-каналом. Если Вы не знаете, как добавить альфа-канал к изображению, то тогда купите себе хорошую книгу, посмотрите в Интернет или прочитайте помощь по Adobe Photoshop. Весь процесс создания изображения очень похож на тот, с помощью которого я создавал маски в уроке маскирования. Загрузите ваш объект в Adobe Photoshop (или другую программу для работы с изображениями, которая поддерживает альфа-канал). Произведите выбор области объекта по его контуру, например, выбором цветового диапазона. Скопируйте эту область. Создайте новое изображение. Вставьте выбранную область в новое изображение. Сделаете инверсию изображения так, чтобы область, где находится ваше изображение, стала черной. Сделайте область вокруг изображения белой. Выберите все изображение, и скопируйте его. Возвратитесь к первоначальному изображению, и создайте альфа-канал. Вставьте черно-белую маску, которую Вы только, что создали в альфа-канал. Сохраните изображение как 32 битный TGA файл. Проверьте, что сохранилась прозрачность, и проверьте, что Вы сохранили несжатое изображение!

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

 

#include <windows.h> // заголовочный файл для Windows

#include <stdio.h>   // заголовочный файл для стандартного ввода/вывода

#include <stdarg.h>  // заголовочный файл для манипуляций с переменными аргументами

#include <gl\gl.h>   // заголовочный файл для библиотеки OpenGL32

#include <gl\glu.h>  // заголовочный файл для библиотеки GLu32

#include <time.h>    // для генерации псевдослучайных чисел

#include "NeHeGL.h"  // заголовочный файл для NeHeGL

 

В уроке 1 я рассказал, как правильно подключить библиотеки OpenGL. В Visual C ++ надо выбрать Project / Settings / Link. И добавить в строчку "Object/library modules" следующую запись: OpenGL32.lib, GLu32.lib и glaux.lib. Если транслятор не сможет подключить нужную библиотеку, то это заставит его извергать ошибку за ошибкой. Именно то, что вы и не хотите! Это все обостряется, когда Вы включили библиотеки в режим отладки, и пробуете скомпилировать ваш в режиме без отладочной информации (release)... еще больше ошибок. Есть много людей, которые просматривают код. Большинство из них плохо знакомы с программированием. Они берут ваш код, и пробуют компилировать его. Они получают ошибки, удаляют код и ищут дальше.

Код ниже сообщит транслятору, что нужно прикомпоновать нужные библиотеки. Немного больше текста, но намного меньше головной боли, в конечном счете. В этом уроке, мы используем библиотеки OpenGL32, GLU32 и WinMM (для проигрывания звука). В этом уроке мы будем загружать TGA файлы, поэтому мы не нуждаемся в библиотеке glaux.

 

#pragma comment( lib, "opengl32.lib" )  // Найти OpenGL32.lib во время линкования

#pragma comment( lib, "glu32.lib" )     // Найти GLu32.lib во время линкования

#pragma comment( lib, "winmm.lib" )     // Найти WinMM во время линкования

 

В трех строках ниже происходит проверка определения компилятором CDS_FULLSCREEN (используется в переключении видеорежима в функции ChangeDisplaySettings). Если это не так, то мы вручную задаем CDS_FULLSCREEN значение 4. Некоторые компиляторы не определяют CDS_FULLSCREEN и возвратят сообщение об ошибки, если CDS_FULLSCREEN используется! Чтобы предотвратить сообщение об ошибке, мы проверяем, был ли CDS_FULLSCREEN определен и если нет, то мы вручную задаем его. Это сделает жизнь немного проще для каждого. (Примечание переводчика: здесь ошибка, это определение нужно перенести в файл NeHeGL.cpp, так как там используется функция ChangeDisplaySettings).

Затем мы объявляем DrawTargets, и задаем переменные для нашего окна и обработки клавиатуры. Если Вы не знаете, как надо определять переменные, то пролистайте MSDN глоссарий. Имейте в виду, я не преподаю C/C++, купите хорошую книгу по этому языку, если Вам нужна справка не по GL коду!

 

#ifndef    CDS_FULLSCREEN     // CDS_FULLSCREEN не определен

#define    CDS_FULLSCREEN 4   // компилятором. Определим его,

#endif                        // чтобы избежать ошибок

 

void DrawTargets();           // декларация

 

GL_Window*  g_window;

Keys*    g_keys;

 

В следующем разделе кода задаются наши пользовательские переменные. Переменная base будет использоваться для наших списков отображения для шрифта. Переменная roll  будет использоваться, чтобы перемещения земли и создания иллюзии прокрутки облаков. Переменная level (уровень) используется по прямому назначению (мы начинаем с уровня 1). Переменная miss отслеживает, сколько объектов не было сбито. Она также используется, чтобы показать боевой дух игрока (если не было промахов, то это означает высокий боевой дух). Переменная kills следит за тем, сколько целей было поражено на каждом уровне. В переменной score сохраняется общее число попаданий в объекты, и переменная game будет использоваться, чтобы сообщить о конце игры!

Последняя строка определяет нашу функцию сравнения. Функция qsort требует последний параметр с типом (const *void, const *void)..

 

// Наши пользовательские переменные

GLuint   base;    // Список отображения для шрифта

GLfloat  roll;    // Прокрутка облаков

GLint    level=1; // Текущий уровень

GLint    miss;    // Пропущенные цели

GLint    kills;   // Счетчик поражений для уровня

GLint    score;   // Текущий счет

bool     game;    // Игра окончена?

 

typedef int (*compfn)(const void*, const void*); // Определение нашей функции сравнения

 

Теперь о нашей структуре для объектов. Эта структура содержит всю информацию об объекте. Направление, в котором он вращается, попадание в него, положение на экране, и т.д.

Краткое описание переменных... Переменная rot определяет направление, в котором мы хотим вращать объект. Переменная hit равна ЛОЖЬ, если в объект еще не попали. Если объект был поражен или вручную помечен как пораженный, значение hit будет ИСТИННА.

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

Чтобы следить за тем, в каком направлении наш объект перемещается, мы имеем переменную называемую dir. Переменная dir может принимать одно из 4 значений: 0 - объект перемещается влево, 1 - объект перемещает вправо, 2 – объект перемещается вверх и, наконец, 3 - объект перемещается вниз.

Переменная texid может иметь любое значение от 0 до 4. Ноль задает текстуру BlueFace (голубая рожа), 1 - текстуру Bucket (ведро), 2 – текстуру Target (мишень), 3 – Coke (банка кока-колы), и 4 - текстура Vase (ваза). Позже в коде загрузке текстуры, Вы увидите, что первые 5 текстур – изображения целей.

Обе переменные x и y используются для позиционирования объекта на экране. Переменная x задает позицию объекта по оси X, а переменная y задает позицию объекта по оси Y.

Объекты вращаются относительно оси Z в зависимости от значения переменной spin. Позже в коде, мы будем увеличивать или уменьшать вращение, основываясь на направлении перемещения объекта.

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

 

struct objects {

  GLuint  rot;       // Вращение (0-нет, 1-по часовой, 2-против)

  bool  hit;         // В объект попали?

  GLuint  frame;     // Текущий кадр взрыва

  GLuint  dir;       // Направление объекта (0-лево, 1-право, 2-вверх, 3-низ)

  GLuint  texid;     // ID текстуры объекта

  GLfloat  x;        // X позиция

  GLfloat y;         // Y позиция

  GLfloat  spin;     // Вращение

  GLfloat  distance; // Расстояние

};

 

Абсолютно не зачем пояснять код ниже. Мы загружаем изображения в формате TGA в этом уроке вместо изображений в формате BMP. Структура ниже используется, чтобы хранить данные изображения, также как информацию об изображении в формате TGA. Прочитайте урок по работе с TGA файлами, если Вы нуждаетесь в детальном объяснении кода ниже.

 

typedef struct         // Создаем структуру

{

  GLubyte  *imageData; // Данные изображения

  GLuint  bpp;         // Цветность изображения в битах на пиксель.

  GLuint  width;       // Ширина изображения

  GLuint  height;      // Высота изображения

  GLuint  texID;       // ID текстуры используемый для выбора текстуры

} TextureImage;        // Имя структуры

 

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

 

TextureImage textures[10]; // Место для хранения 10 текстур

 

objects  object[30];       // Место для хранения 30 объектов

 

Я не хочу ограничивать размер каждого объекта. Я хочу, чтобы ваза была более высокой, чем ведро, а ведро было более широкое, чем ваза. Чтобы упростить жизнь, я создаю структуру, в которой есть ширина объектов (w) и высота (h).

Затем я задаю ширину и высоту каждого объекта в последней строке кода. Чтобы получить ширину банки кока-колы (coke), я буду использовать вызов size[3].w. Для голубой рожи (Blueface) - 0, ведро (Bucket) - 1, мишень (Target) - 2, банка кока-колы (Coke) - 3 и ваза (Vase) - 4. Ширина задается w. Понятно?

 

struct dimensions {        // Размеры объекта

  GLfloat  w;              // Ширина объекта

  GLfloat h;               // Высота объекта

};

 

// Размеры для объектов:Blueface,     Bucket,      Target,       Coke,         Vase

dimensions size[5] = { {1.0f,1.0f}, {1.0f,1.0f}, {1.0f,1.0f}, {0.5f,1.0f}, {0.75f,1.5f} };

 

Следующий большой раздел кода загружает наше TGA изображение и конвертирует ее в текстуру. Это - тот же самый код, так который я использовал в уроке 25, если Вы нуждаетесь в детальном описании его, обратитесь к этому уроку.

Я использую изображения TGA, потому что у них есть возможность сохранять альфа-канал. Альфа-канал сообщает OpenGL, какие части изображения будут прозрачными, а какие части непрозрачны. Альфа-канал создается в программе редактирования изображений, и сохраняется вместе с изображением в файле TGA. OpenGL загружает изображение, и использует альфа-канал для задания величины прозрачности каждого пикселя в изображении.

 

bool LoadTGA(TextureImage *texture, char *filename)   // Загрузка TGA файла в память

{   

  GLubyte    TGAheader[12]={0,0,2,0,0,0,0,0,0,0,0,0}; // Заголовок несжатого TGA

  GLubyte    TGAcompare[12]; // Используется для сравнения заголовка TGA

  GLubyte    header[6];      // Первые 6 полезных байт заголовка

  GLuint     bytesPerPixel;  // Число байт на пиксель в файле TGA

  GLuint     imageSize;      // Используется для сохранения размера изображения

  GLuint     temp;           // Временная переменная

  GLuint     type=GL_RGBA;   // Режим GL по-умолчанию RBGA (32 BPP)

 

  FILE *file = fopen(filename, "rb"); // Открыть TGA файл

 

  // Если файл существует и 12 байт прочитаны и заголовок совпал и прочитаны

  // следующие 6 байт, то все нормально, иначе не удача

  if( file==NULL ||

    fread(TGAcompare,1,sizeof(TGAcompare),file)!=sizeof(TGAcompare) ||

    memcmp(TGAheader,TGAcompare,sizeof(TGAheader))!=0 ||

    fread(header,1,sizeof(header),file)!=sizeof(header))

  {

    if (file == NULL)          // Файл не существует? *Добавлено Jim Strong*

      return FALSE;            // вернуть FALSE

    else                       // иначе

    {

      fclose(file);            // Закрыть файл

      return FALSE;            // вернуть FALSE

    }

  }

 

  texture->width  = header[1] * 256 + header[0]; // Определить ширину (highbyte*256+lowbyte)

  texture->height = header[3] * 256 + header[2]; // Определить высоту (highbyte*256+lowbyte)

   

   if( texture->width <=0 ||             // Ширина меньше или равна чем 0

       texture->height <=0 ||            // Высота меньше или равна чем 0

       (header[4]!=24 && header[4]!=32)) // TGA 24 или 32 бита?

  {

    fclose(file);              // Если не так, то закрыть файл

    return FALSE;              // вернуть FALSE

  }

 

  texture->bpp = header[4];             // Получить число бит на пиксель (24 или 32)

  bytesPerPixel = texture->bpp/8;       // Разделить на 8, чтобы получить байт на пиксель

                                        // Вычислить размер памяти для данных TGA

  imageSize = texture->width*texture->height*bytesPerPixel;

 

  texture->imageData=(GLubyte *)malloc(imageSize); // Выделить память для данных TGA

 

  if( texture->imageData==NULL ||       // Память выделена?

       // Прочитано верное число байт?

      fread(texture->imageData, 1, imageSize, file)!=imageSize)

  {

    if(texture->imageData!=NULL) // Если память выделена

       free(texture->imageData); // Освободить память

 

    fclose(file);                // Закрыть файл

    return FALSE;                // вернуть FALSE

  }

 

  for(GLuint i=0; i<int(imageSize); i+=bytesPerPixel) // Цикл по данным

  {                  // Смена местами первого и третьего байтов ('R'ed и 'B'lue)

    temp=texture->imageData[i]; // Временно сохраняем значение

    texture->imageData[i] = texture->imageData[i + 2]; // Третий байт на место первого

    texture->imageData[i + 2] = temp; // Первый байт на место третьего

  }

 

  fclose (file); // Закрыть файл

 

  // Построить текстуру из данных

  glGenTextures(1, &texture[0].texID); // Генерировать ID текстуры OpenGL

 

  glBindTexture(GL_TEXTURE_2D, texture[0].texID); // Привязка текстуры

  glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);  // Линейная

  glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);  // фильтрация

 

  if (texture[0].bpp==24) // Если TGA 24 бита

  {

    type=GL_RGB;          // Тогда 'type' равен GL_RGB

  }

 

  glTexImage2D(GL_TEXTURE_2D, 0, type,

               texture[0].width, texture[0].height,

               0, type, GL_UNSIGNED_BYTE, texture[0].imageData);

 

  return true; // Текстура построена, вернуть True

}

 

Код вывода шрифта из 2D текстуры тот же самый, который я использовал в предыдущих уроках. Однако есть несколько небольших изменений. Первое, что Вы можете заметить это то, что мы генерируем только 95 списков отображения. Если Вы посмотрите на текстуру шрифта, Вы увидите, что там есть только 95 символов. Второе, что Вы можете заметить то, что мы делим на 16.0f для получения cx, и мы делим на 8.0f для cy. Это делается, потому что текстура шрифта широкая (256 пикселей), а высота равна половине ширины (128 пикселей). Поэтому, вычисляя cx, мы делим на 16.0f, и, вычисляя cy, мы делим на 8.0f.

Если Вы не понимаете код ниже, возвратитесь к уроку 17. Код вывода шрифта подробно объясняется в уроке 17!

 

GLvoid BuildFont(GLvoid)              // Построить наш список отображения для фонта

{

  base=glGenLists(95);                // Создание 95 списков отображения

  glBindTexture(GL_TEXTURE_2D, textures[9].texID); // Привязка нашей текстуры фонта

  for (int loop=0; loop<95; loop++)   // Цикл по спискам

  {

    float cx=float(loop%16)/16.0f;    // X позиция текущего символа

    float cy=float(loop/16)/8.0f;     // Y позиция текущего символа

 

    glNewList(base+loop,GL_COMPILE);  // Начало построения списка

      glBegin(GL_QUADS);              // Использовать четырехугольник для символа

        // Координаты текстуры / вершин (Низ Лево)

        glTexCoord2f(cx,          1.0f-cy-0.120f); glVertex2i(0,0);

        // Координаты текстуры / вершин (Низ Право)

        glTexCoord2f(cx+0.0625f, 1.0f-cy-0.120f);  glVertex2i(16,0);

        // Координаты текстуры / вершин (Верх Право)

        glTexCoord2f(cx+0.0625f, 1.0f-cy);         glVertex2i(16,16);

        // Координаты текстуры / вершин (Низ Лево)

        glTexCoord2f(cx,         1.0f-cy);         glVertex2i(0,16);

      glEnd();                        // Конец построения нашего четырехугольника (символ)

      glTranslated(10,0,0);           // Сдвиг вправо на символ

    glEndList();                      // Завершение построения списка отображения

  }                                   // Цикл по всем символам

}

 

Код вывода строки такой же, как в уроке 17, но был изменен, чтобы дать возможность нам вывести счет, уровень и мораль на экран (переменные, которые непрерывно изменяются).

 

GLvoid glPrint(GLint x, GLint y, const char *string, ...) // Здесь печать

{

  char       text[256];           // Место для строки

  va_list    ap;                  // Указатель на список аргументов

 

  if (string == NULL)             // Если нет текста

    return;                       // то ничего не делаем

 

  va_start(ap, string);           // Разбор строки из переменных

      vsprintf(text, string, ap); // Конвертирование символов в числа

  va_end(ap);                     // Значения сохраняются в текст

 

  glBindTexture(GL_TEXTURE_2D, textures[9].texID);   // Выбор нашей текстуры шрифта

  glPushMatrix();                 // Сохранить матрицу вида модели

  glLoadIdentity();               // Сброс матрицы вида модели

  glTranslated(x,y,0);            // Позиционирование текста (0,0 – низ лево)

  glListBase(base-32);            // Выбор набора символов

  glCallLists(strlen(text), GL_UNSIGNED_BYTE, text); // Нарисовать текст списками

  glPopMatrix();                  // Восстановить матрицу

}

 

Этот код будет вызываться позже из функции qsort. Он нужен для сравнения расстояний в двух структурах и возвращает значение -1, если расстояние в первой структуре было меньше чем во второй, и значение 1, если расстояние в первой структуре больше чем во второй, и значение 0, если расстояние в обеих структурах равно.

 

int Compare(struct objects *elem1, struct objects *elem2) // Функция сравнения

{

   // Если расстояние в первой структуре меньше чем во второй

   if ( elem1->distance < elem2->distance)

      return -1; // Вернуть –1

   // Если расстояние в первой структуре больше чем во второй

   else if (elem1->distance > elem2->distance)

      return 1;  // Вернуть 1

   else          // Иначе (Если расстояние равно)

      return 0;  // Вернуть 0

}

 

В коде функции InitObject() мы инициализируем каждый объект. Мы начинаем, с установки rot в 1. При этом объект будет вращаться по часовой стрелке. Затем мы инициализируем анимацию взрыва для кадра 0 (мы не хотим, чтобы взрыв был показан с половины анимационной последовательности). Затем мы устанавливаем hit в ЛОЖЬ, что означает, что объект еще не был сбит или подвергся самоуничтожению. Для выбора текстурного объекта, переменной texid присваивается случайное значение от 0 до 4. Ноль - текстура blueface, и 4 - текстура vase. Это даст нам один из 5 случайных объектов.

Переменной distance будет присвоено случайное число от -0.0f до -40.0f (4000/100 равно 40). Когда мы будем рисовать объект, мы смещаем его на 10 единиц в экран. Поэтому, когда объект нарисован, он будет нарисован от -10.0f до -50.0f единиц в глубине экрана (и не близко и не далеко). Я делю случайное число на 100.0f, чтобы получить более точное значение с плавающей запятой.

После того как мы задали случайное значение дистанции до объекта, мы задаем объекту случайное значение по y. Мы не хотим, чтобы объект был ниже чем -1.5f, иначе он будет под землей, и мы не хотим, чтобы объект был выше, чем 3.0f. Поэтому диапазон наших случайных чисел не может быть больше чем 4.5f (-1.5f+4.5f=3.0f).

Для вычисления позиции x, мы используем довольно хитрый способ. Мы берем наше расстояние, и мы вычитаем 15.0f из него. Затем мы делим результат на 2 и вычитаем 5*level. Наконец, мы вычитаем случайное число от 0.0f до 5 умноженное на текущий уровень. Мы вычитаем 5*level и случайное число от 0.0f до 5*level так, чтобы наш объект появлялся дальше от экрана на более высоких уровнях. Если бы мы не делали этого, объекты появились бы один за другим, при этом попасть в них было бы труднее, чем в этом варианте.

Наконец мы выбираем случайное направление (dir) от 0 (слева) до 1 (направо).

Вот пример, чтобы вам было все это понятно. Скажем, что расстояние равно -30.0f, а текущий уровень равен 1:

 

object[num].x=((-30.0f-15.0f)/2.0f)-(5*1)-float(rand()%(5*1));
object[num].x=(-45.0f/2.0f)-5-float(rand()%5);
object[num].x=(-22.5f)-5-{давайте скажем 3.0f};
object[num].x=(-22.5f)-5-{3.0f};
object[num].x=-27.5f-{3.0f};
object[num].x=-30.5f;

Запомним, что мы сдвигаем на 10 единиц в экран прежде, чем мы выводим наши объекты, и расстояние в примере выше равно -30.0f. Можно с уверенностью сказать, что наше фактическое расстояние вглубь экрана будет равно -40.0f. Используя код перспективы из файла NeHeGL.cpp, с уверенностью можно будет предположить, что, если расстояние равно -40.0f, левый край экрана будет -20.0f, и правый край будет +20.0f. В коде выше наше значение x равно -22.5f (т.е. за левым краем экрана). Затем мы вычитаем 5 и наше случайное значение 3, которое гарантирует, что объект начнет перемещаться за экраном (с -30.5f) - это означает, что объект должен будет переместить приблизительно на 8 единиц вправо прежде, чем он появится на экране.

GLvoid InitObject(int num)     // Инициализация объекта

{

  object[num].rot=1;           // Вращение по часовой

  object[num].frame=0;         // Сброс кадра взрыва в ноль

  object[num].hit=FALSE;       // Сброс статуса попадания в объект

  object[num].texid=rand()%5;  // Назначение новой текстуры

  object[num].distance=-(float(rand()%4001)/100.0f);  // Случайная дистанция

  object[num].y=-1.5f+(float(rand()%451)/100.0f);     // Случайная Y позиция

  // Случайное начальная X позиция, основанная на расстоянии объекта

  // и случайном числе для задержки (Положительное значение)

  object[num].x=((object[num].distance-15.0f)/2.0f)-(5*level)-float(rand()%(5*level));

  object[num].dir=(rand()%2);  // Взять случайное направление

 

Теперь мы проверим, в каком направлении объект собирается лететь. Код ниже проверяет, перемещается ли объект влево. Если это так, то мы должны изменить вращение так, чтобы объект вращался против часовой стрелки. Мы делаем это, изменяя значение rot на 2.

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

 

  if (object[num].dir==0)          // Направление вправо

  {

    object[num].rot=2;             // Вращение против часовой стрелки

    object[num].x=-object[num].x;  // Начнем с левой стороны (Отрицательное значение)

  }

 

Теперь мы проверяем texid, чтобы выяснить какой случайный объект компьютер выбрал. Если texid равно 0, то компьютер выбрал объект blueface. Парни с голубыми рожами всегда катятся по земле. Чтобы быть уверенными, что они начинают свое путешествие на наземном уровне, мы принудительно устанавливаем значение y в -2.0f.

 

  if (object[num].texid==0)        // Голубая рожа

    object[num].y=-2.0f;           // Всегда катится по земле

 

Затем мы проверяем, равно ли texid 1. Если это так, то компьютер выбрал ведро. Ведро не путешествует слева направо, оно падает с неба. Поэтому вначале мы должны установить dir в 3. Это сообщит компьютеру, что наш ковш падает или перемещается вниз.

Наш код инициализации предполагает, что объект будет путешествовать слева направо. Поскольку ковш падает, мы должны дать ему новое случайное значение по x. Если бы мы этого не делали, ковш никогда не был бы видим. Он падал бы или за  левым краем экрана или за правым краем экрана. Чтобы назначить новое значение, мы выбираем случайное значение, которое получается на основании расстояния от экрана. Вместо того чтобы вычитать 15, мы вычитаем только 10. Это дает нам более маленький диапазон, и сохраняет объект на экране. Назначив наше расстояние в -30.0f, мы будем иметь случайное значение от 0.0f до 40.0f. Если Вы спрашиваете себя, почему от 0.0f до 40.0f? Разве оно не должно быть от 0.0f до -40.0f? Ответ прост. Функция rand() всегда возвращает положительное число. Поэтому независимо от числа, мы получим обратно положительное значение. Так или иначе... вернемся. Поэтому мы имеем положительное число от 0.0f до 40.0f. Затем мы добавляем расстояние (отрицательное значение) минус 10.0f деленное на 2. Для примера ... пусть случайное значение 15, а расстояние равно -30.0f:

object[num].x=float(rand()%int(-30.0f-10.0f))+((-30.0f-10.0f)/2.0f);
object[num].x=float(rand()%int(-40.0f)+(-40.0f)/2.0f);
object[num].x=float(15 {пусть будет 15))+(-20.0f);
object[num].x=15.0f-20.0f;
object[num].x=-5.0f;

В завершении мы должны задать значение y. Мы хотим, чтобы ведро падало с неба. Мы не хотим, что бы оно проходило сквозь облака. Поэтому мы устанавливаем значение y в 4.5f. Немного ниже облаков.

  if (object[num].texid==1)        // Ведро

  {

    object[num].dir=3;             // Падает вниз

    object[num].x=float(rand()%int(object[num].distance-10.0f))+

                   ((object[num].distance-10.0f)/2.0f);

    object[num].y=4.5f;            // Случайное X, начинаем с верха экрана

  }

 

Мы хотим, чтобы мишень вылетела из земли и поднялась в воздух. Мы проверяем объект действительно мишень (texid ранвно 2). Если это так, то мы задаем направление (dir) равным 2 (верх). Мы используем точно тот же самый код, как и выше, чтобы получить случайное положение по x.

Мы не хотим, чтобы мишень вылетала выше земли. Поэтому мы задаем начальное значение y равным -3.0f (под землей). Затем мы вычитаем случайное значение от 0.0f до 5 умноженное на текущий уровень. Мы делаем это затем, чтобы мишень НЕМЕДЛЕННО НЕ появилась. На более высоких уровнях мы хотим ввести задержку прежде, чем мишень появится. Без задержки, мишени бы вылетали одна за другой, отводя Вам, мало времени, чтобы сбить их.

 

  if (object[num].texid==2)       // Мишень

  {

    object[num].dir=2;            // Вверх

    // Случайное X, старт из под земли + случайное значение

    object[num].x=float(rand()%int(object[num].distance-10.0f))+

                  ((object[num].distance-10.0f)/2.0f);

    object[num].y=-3.0f-float(rand()%(5*level));

  }

 

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

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

Причина этого проста... Z буфер не допускает рисование OpenGL пикселей, которые уже сзади выведенных пикселей. Поэтому может так случится, что объекты, выведенные сзади прозрачных объектов, не обнаруживаются. Поэтому Вы можете увидеть квадратную форму вокруг перекрывающихся объектов... Не хорошо!

Мы уже знаем глубину каждого объекта. Поэтому после инициализации нового объекта, мы можем обойти эту проблему, сортируя объекты, используя функцию qsort (быстрая сортировка). При помощи сортировки объектов, мы можем убедиться, что первый выведенный объект – это тот объект, который дальше всего. Поэтому, когда мы выводим объекты, мы начинаем с первого объекта, дальние объекты будут выведены вначале. Ближние объекты будут видеть предварительно выведенные объекты позади них, и смешивание пройдет должным образом!

Как отмечено в комментариях ниже: я нашел этот код в MSDN после нескольких часов поиска в Интернете. Этот код работает хорошо и позволяет Вам сортировать структуры целиком. Функция qsort имеет 4 параметра. Первый параметр указывает на массив объектов (массив, который нужно сортировать). Второй параметр - число массивов, которые мы хотим сортировать... Конечно, мы хотим сортировать все объекты, которые в настоящее время отображаются (число объектов задается уровнем). Третий параметр определяет размер нашей структуры объектов, и четвертый параметр указывает на нашу функцию сравнения.

Есть, вероятно, более лучший способ сортировать структуры, но qsort () работает... Это быстро и удобно!

Важно обратить внимание на то, что, если Вы хотите использовать glAlphaFunc() и glEnable (GL_ALPHA_TEST), в сортировке нет необходимости. Однако, используя только альфа-тест (позволяет принять или отклонить фрагмент, основываясь на значение его альфа-канала, но не смешивает) Вы ограничены полностью прозрачным или полностью непрозрачным смешиванием, но при этом не возникает реального смешивания. С сортировкой и использованием Blendfunc() надо немного больше работы, но при этом учитываются полупрозрачные объекты.

 

  // Сортировка объектов по расстоянию:

  //  *** Код MSDN модифицирован в этом уроке ***

  //                Начальный адрес нашего массива объектов

  //                Число сортируемых элементов

  //                Размер каждого элемента

  //                Указатель на нашу функцию сравнения

  qsort((void *) &object, level, sizeof(struct objects), (compfn)Compare );

}

 

Код инициализации тот же самый, как и всегда. В первых двух строках сохраняется информация о нашем окне и нашем обработчике клавиатуры. Затем используем srand() чтобы сделать нашу игру более случайную, основываясь на времени. После этого мы загружаем наши TGA изображения и конвертируем их в текстуры, используя LoadTGA(). Первые 5 изображений - объекты, которые будут летать по экрану. Далее, загрузим текстуры Explode (взрыв) – анимация взрыва, ground (земля) и sky (небо) - фон сцены, crosshair (перекрестье) - курсор, который показывает текущее положение мыши на экране, и, наконец, font - шрифт для отображения счета, заголовка, и морали. Если какое-то из изображений не загрузиться, будет возвращено ЛОЖЬ, и программа закроется. Важно обратить внимание на то, что этот основной код не будет выводить сообщение о неудавшейся инициализации.

 

BOOL Initialize (GL_Window* window, Keys* keys) // Инициализация OpenGL

{

  g_window  = window;

  g_keys    = keys;

 

  srand( (unsigned)time( NULL ) );          // Привнесение случайности

 

  if ((!LoadTGA(&textures[0],"Data/BlueFace.tga")) ||// Загрузка текстуры BlueFace

    (!LoadTGA(&textures[1],"Data/Bucket.tga")) ||    // Загрузка текстуры Bucket

    (!LoadTGA(&textures[2],"Data/Target.tga")) ||    // Загрузка текстуры Target

    (!LoadTGA(&textures[3],"Data/Coke.tga")) ||      // Загрузка текстуры Coke

    (!LoadTGA(&textures[4],"Data/Vase.tga")) ||      // Загрузка текстуры Vase

    (!LoadTGA(&textures[5],"Data/Explode.tga")) ||   // Загрузка текстуры Explosion

    (!LoadTGA(&textures[6],"Data/Ground.tga")) ||    // Загрузка текстуры Ground

    (!LoadTGA(&textures[7],"Data/Sky.tga")) ||       // Загрузка текстуры Sky

    (!LoadTGA(&textures[8],"Data/Crosshair.tga")) || // Загрузка текстуры Crosshair

    (!LoadTGA(&textures[9],"Data/Font.tga")))        // Загрузка текстуры Font

  {

    return FALSE;              // Если не удачно, то вернем False

  }

 

Если все изображения были загружены и конвертированы в текстуры успешно, мы можем продолжать инициализацию. Текстура шрифта загружена, поэтому можно создать наш шрифт. Мы делаем это, вызывая BuildFont().

Затем мы настраиваем OpenGL. Цвет фона - черный, альфа в 0.0f. Буфер глубины включен и разрешен с тестом меньше или равно.

Функция glBlendFunc() - очень важная строка кода. Мы задаем функцию смешивания как (GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA). При этом объект смешивается со всем, что на экране, используя альфа значения, которые есть в текстуре объекта. После настройки режима смешивания, мы разрешаем смешивание. Затем разрешаем 2D наложение текстуры, и, наконец, мы разрешаем GL_CULL_FACE. При этом удаляются задние грани каждого объекта (нет смысла выводить то, что мы не видим). Мы выводим все наши четырехугольники против часовой стрелки, поэтому будет выбрана правильная грань.

Ранее я говорил об использовании glAlphaFunc() вместо альфа смешивания. Если Вы хотите использовать альфа тест, закомментируйте 2 строки кода смешивания и уберите комментарий с 2 строк сразу за glEnable(GL_BLEND). Вы можете также закомментировать функцию qsort() в InitObject().

Программа запуститься, но текстуры неба при этом не будет. Причина этого в том, что текстура неба имеет альфа-значение равное 0.5f. Когда я говорил об альфа тесте раньше, я упомянул, что он работает только с альфа значениями 0 или 1. Вы должны будете изменить альфа канал для текстуры неба, если Вы хотите, чтобы оно появилось! Еще раз, если Вы хотите использовать альфа тест, Вы не должны сортировать объекты. Оба метода хороши! Ниже небольшая цитата с сайта SGI:

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

 

  BuildFont();                    // Построение списков отображения для шрифта

 

  glClearColor(0.0f, 0.0f, 0.0f, 0.0f); // Черный фон

  glClearDepth(1.0f);             // Настройка буфера глубины

  glDepthFunc(GL_LEQUAL);         // Тип теста глубины

  glEnable(GL_DEPTH_TEST);        // Разрешен тест глубины

  // Разрешено альфа-смешивание (запрет альфа теста)

  glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

  glEnable(GL_BLEND);              // Разрешено смешивание  (запрет альфа теста)

//  glAlphaFunc(GL_GREATER,0.1f);  // Настройка альфа теста (запрет смешивания)

//  glEnable(GL_ALPHA_TEST);       // Разрешен альфа тест   (запрет смешивания)

  glEnable(GL_TEXTURE_2D);         // Разрешено наложение текстуры

  glEnable(GL_CULL_FACE);          // Удалить заднюю грань

 

До этой части программы, ни один из объектов не был задан. Поэтому мы делаем цикл по всем тридцати объектам, вызывая InitObject() для каждого объекта.

 

  for (int loop=0; loop<30; loop++) // Цикл по 30 объектам

    InitObject(loop);               // Инициализация каждого объекта

 

  return TRUE;                      // Возврат TRUE (Инициализация успешна)

}

 

В нашем коде инициализации, мы вызывали BuildFont(), для создания наших 95 списков отображения. Следующая строка кода удаляет все 95 списков отображения перед выходом программы.

 

void Deinitialize (void)            // Любая пользовательская деинициализация здесь

{

  glDeleteLists(base,95);           // Уничтожить все 95 списков отображения фонта

}

 

Теперь немного хитрого кода ... Кода, который фактически делает выбор объектов. В первой строке кода ниже создается буфер, который мы можем использовать, чтобы хранить информацию о наших выбранных объектах. В переменной попадания (hits) будет храниться число обнаруженных попаданий в режиме выбора.

 

void Selection(void)                // Здесь происходит выбор

{

  GLuint  buffer[512];              // Настройка буфера выбора

  GLint  hits;                      // Число объектов, которые мы выбрали

 

Вначале, мы проверяем, окончена ли игра. Если это так, то нет никакого смысла выбирать, поэтому мы производим выход из функции. Если игра еще не окончена, мы проигрываем звук выстрела с использование функции Playsound(). Функция Selection() вызывается только тогда, когда кнопка мыши была нажата, и каждый раз, когда кнопка мыши нажата, мы хотим проиграть звук выстрела. Звук запускается в асинхронном режиме для того, чтобы не остановить программу, во время проигрывания звука.

 

  if (game)                // Игра окончена?

    return;                // Если так, то выбирать нечего

 

  PlaySound("data/shot.wav",NULL,SND_ASYNC); // Проигрывание звука выстрела

 

Теперь мы настроим область просмотра. Массив viewport[] хранит x, y, длину и ширину текущей области просмотра (окно OpenGL).

Функция glGetIntegerv(GL_VIEWPORT, viewport) возвращает границы текущей области просмотра и помещает viewport[]. Первоначально, границы равны размерам окна OpenGL. Вызов функции glSelectBuffer(512, buffer) сообщает OpenGL, что надо использовать buffer для буфера выбора.

 

  // Размер области просмотра. [0] - <x>, [1] - <y>, [2] - <length>, [3] - <width>

  GLint  viewport[4];

 

  // Помещаем в массив <viewport> размеры и положение на экране относительно окна

  glGetIntegerv(GL_VIEWPORT, viewport);

  glSelectBuffer(512, buffer); // Скажем OpenGL использовать этот массив для выбора

 

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

Далее мы инициализируем стек имен, вызывая glInitNames() и glPushName(0). Важно обратить внимание на то, что, если программа не в режиме выбора, запрос к glPushName() будет игнорирован. Конечно, мы находимся в режиме выбора, но это важно иметь в виду.

 

  // Переключить OpenGL в режим выбора. Ничего не будет нарисовано.

  // Идентификатор объекта и его размеры будут сохранены в буфере.

  (void) glRenderMode(GL_SELECT);

 

  glInitNames();                // Инициализация стека имен

  glPushName(0);                // Поместить 0 в стек (наименьший первый элемент)

 

После подготовки стека имен, мы должны ограничить область рисования только под нашим курсором. Чтобы это сделать, мы должны выбрать матрицу проецирования. После выбора матрицы проецирования мы помещаем ее в стек. Затем сбрасываем матрицу проецирования используя glLoadIdentity().

Мы ограничим рисование, используя gluPickMatrix() (задает область выбора). Первый параметр - наша текущая позиция мыши по оси X, второй параметр - текущая позиция мыши по оси Y, затем ширина и высота области выбора. Наконец, viewport[]. Массив viewport[] указывает текущие границы области просмотра. Переменные mouse_x и mouse_y будут в центре области выбора.

 

  glMatrixMode(GL_PROJECTION);   // Выбор матрицы проецирования

  glPushMatrix();                // Поместить матрицу проецирования

  glLoadIdentity();              // Сброс матрицы

 

  // Создание матрицы, которая будет задавать маленькую часть экрана под мышью.

  gluPickMatrix((GLdouble) mouse_x, (GLdouble) (viewport[3]-mouse_y), 1.0f, 1.0f, viewport);

 

Вызов gluPerspective() умножает перспективную матрицу на матрицу выбора, которая ограничивает область рисования, которая задана gluPickMatrix().

Затем мы выбираем матрицу вида модели и выводим наши цели при помощи вызова DrawTargets(). Мы выводим цели в DrawTargets(), а не в Draw() потому что мы хотим, чтобы в режиме выбора были проверены попадания только в объекты (цели), а не с небом, землей или курсором.

После отрисовки наших целей, мы выбираем снова матрицу проецирования и возвращаем сохраненную матрицу из стека. Затем мы выбираем снова матрицу вида модели.

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

 

  // Применить перспективную матрицу

  gluPerspective(45.0f, (GLfloat) (viewport[2]-viewport[0])/(GLfloat) (viewport[3]-viewport[1]),

                 0.1f, 100.0f);

  glMatrixMode(GL_MODELVIEW);   // Выбор матрицы вида модели

  DrawTargets();                // Визуализация целей в буфер выбора

  glMatrixMode(GL_PROJECTION);  // Выбор матрицы проецирования

  glPopMatrix();                // Получить матрицу проецирования

  glMatrixMode(GL_MODELVIEW);   // Выбор матрицы вида модели

  hits=glRenderMode(GL_RENDER); // Выбор режима визуализации, найти как много

 

Теперь мы проверяем больше ли нуля число зарегистрированных попаданий. Если это так, то мы присваиваем переменной choose имя первого объекта, нарисованного в области выбора. Переменной depth присваивается значение расстояния объекта от экрана (глубина).

Каждое попадание помещает 4 элемента в буфер. Первый элемент - число имен в стеке имен, когда произошло попадание. Второй элемент - значение минимума z всех вершин, которые пересекают видимую область во время попадания. Третий элемент - значение максимума z всех вершин, которые пересекают видимую область во время попадания, и последний элемент – содержимое стека имен во время попадания (имя объекта). В этом уроке нам необходимо только значение минимума z и имя объекта.

 

  if (hits > 0)                // Если есть попадания

  {

    int  choose = buffer[3];   // Сделать наш выбор первым объектом

    int  depth = buffer[1];    // Сохранить как далеко он

 

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

Поэтому, мы проверяем все попадания. Вспомните, что на каждый объект приходится 4 элемента в буфере, поэтому чтобы перейти на следующее попадание мы должны умножить текущее значение цикла на 4. Мы добавляем 1, чтобы получить глубину каждого объекта. Если глубина - меньше чем текущая глубина выбранного объекта, мы сохраняем имя более близкого объекта в choose, и мы сохраняем в depth глубину более близкого объекта. После завершения цикла по всем нашим попаданиям, в choose будет находиться имя самого близкого объекта, а в depth будет содержаться глубина его.

 

    for (int loop = 1; loop < hits; loop++) // Цикл по всем обнаруженным попаданиям

    {

      // Если этот объект ближе, чем выбранный

      if (buffer[loop*4+1] < GLuint(depth))

      {

        choose = buffer[loop*4+3];          // Выбрать ближний объект

        depth = buffer[loop*4+1];           // Сохранить как далеко он

      }      

    }

 

Все, что мы должны сделать – пометить, что в этот объект попали. Мы проверяем, что объект уже не был помечен. Если он не был помечен, то мы отмечаем, что в него попали, задавая hit ИСТИНА. Мы увеличиваем счет игрока на единицу, и мы увеличиваемся счетчик поражений на 1.

 

    if (!object[choose].hit)          // Если в объект еще не попадали

    {

      object[choose].hit=TRUE;        // Пометить, что в объект попали

      score+=1;            // Увеличить счет

      kills+=1;            // Увеличить число поражений

 

Я использую kills, чтобы знать, сколько объектов были уничтожены на каждом уровне. Я хочу на каждом уровне иметь большее количество объектов (чтобы каждый следующий уровень проходился тяжелее). Поэтому я проверяю, если игрок уничтожил объектов, больше чем значение текущего уровня, умноженного на 5, то он переходит на следующий уровень. На уровне 1, игрок должен уничтожить 5 объектов (1*5). На уровне 2 игрок должен уничтожить 10 объектов (2*5). Уровень трудности прогрессивно повышается.

Поэтому, в первой строке кода проверки смотрим, выше ли число поражений, чем уровень, умноженный на 5. Если это так, то мы устанавливаем miss в 0. Это задает боевой дух игрока обратно в 10 из 10 (боевой дух равен с 10-miss). Затем устанавливаем число поражений в 0 (начинаем процесс подсчета снова).

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

Очень важно обратить внимание, на что Вы можете иметь максимум 64 объекта на экране (0-63). Если Вы пробуете визуализировать 65 или более объектов, выбор становится путанным, и не верным. Каждый из объектов, случайно может привести к остановке вашего компьютера. Это - физический предел в OpenGL (точно так же как 8 источников света).

Если вдруг Вы - бог, и Вы закончили уровень 30, level больше не будет увеличиваться, и ваш счет тоже. Ваш боевой дух также сбросится к 10, каждый раз Вы, когда вы заканчиваете 30-ый уровень.

 

      if (kills>level*5)   // Новый уровень?

      {

        miss=0;            // Сброс числа промахов

        kills=0;           // Сброс число поражений

        level+=1;          // Увеличение уровня

        if (level>30)      // Больше чем 30?

          level=30;        // Поэтому уровень30 (Вы бог?)

      }

    }

  }

}

 

В функции Update() проверяется нажатие клавиш, и обновляется положение объектов. Одна из приятных особенностей Update() – миллисекундный таймер. Вы можете использовать миллисекундный таймер, чтобы переместить объекты, основываясь на времени, которое прошло, с того момента, как Update() была вызвана последний раз. Важно обратить внимание на то, что движущийся объект будет перемещаться с той же самой скоростью на любом процессоре... НО есть и недостатки! Пусть имеется объект, который перемещается на 5 единиц за 10 секунд. На быстрой системе, компьютер будет перемещать объект на половину единицы за каждую секунду. На медленной системе, это произойдет через 2 секунды прежде, чем даже процедура обновления будет вызвана. Поэтому, когда объект двигается, будет казаться, что он дергается. Мультипликация не будет плавной на более медленной системе. (Примечание: это несколько преувеличенный пример... любой компьютер будет обновлять экран быстрее, чем раз в две секунды).

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

 

void Update(DWORD milliseconds)             // Обновление движения здесь

{

  if (g_keys->keyDown[VK_ESCAPE])           // ESC нажата?

  {

    TerminateApplication (g_window);        // Прервать программу

  }

 

Далее проверяем, нажата ли клавиша "пробел", и при этом игра окончена. Если оба условия истинны, мы инициализируем все 30 объектов (даем им новые направления, присваиваем текстуры, и т.д). Мы устанавливаем game в ЛОЖЬ, сообщая программе, что игра продолжается. Мы сбрасываем score в 0, level в 1, kills в 0, и, наконец, мы устанавливаем переменную miss в ноль. При этом перезапуск игры будет с первым уровнем, с полной моралью и счетом 0.

 

  if (g_keys->keyDown[' '] && game)          // Пробел нажат после того как окончена игра?

  {

    for (int loop=0; loop<30; loop++)        // Цикл по 30 объектам

      InitObject(loop);      // Инициализация каждого объекта

 

    game=FALSE;              // Установка game в False

    score=0;                 // Установка score в 0

    level=1;                 // Установка level в 1

    kills=0;                 // Установка Kills в 0

    miss=0;                  // Установка miss в 0

  }

 

В коде ниже проверяет нажатие клавиши F1. Если клавиша F1 нажата, то ToggleFullscreen переключит из оконного в полноэкранный режим или из полноэкранного режима в оконный режим.

 

  if (g_keys->keyDown[VK_F1])            // F1 нажата?

  {

    ToggleFullscreen (g_window);         // Переключение видеорежима

  }

 

Для создания иллюзии движущихся облаков и перемещения земли, мы уменьшаем roll на 0.00005f, умноженное на число прошедших миллисекунд. При этом облака будут перемещаться с той же самой скоростью на всех системах (быстро или медленно).

Затем создаем цикл по всем объектам на экране. На уровне 1 имеется один объект, на уровне 10 имеется 10 объектов, и т.д.

 

  roll-=milliseconds*0.00005f;            // Прокрутка облаков

 

  for (int loop=0; loop<level; loop++)    // По всем объектам

  {

 

Мы должны выяснить, каким способом объект должен вращаться. Мы делаем это при помощи проверки значения rot. Если rot равняется 1, мы должны вращать объект по часовой стрелке. Для того чтобы сделать это, мы уменьшаем значение spin. Мы уменьшаем spin на 0.2f, умноженное на значение loop плюс число прошедших миллисекунд. Используя миллисекунды, объекты будут вращать с той же самой скоростью на всех системах. Добавляя loop, мы заставляем каждый НОВЫЙ объект вращаться чуточку быстрее, чем последний объект. Поэтому второй объект будет вращаться быстрее, чем первый объект, и третий объект будет вращаться быстрее, чем второй объект.

 

    if (object[loop].rot==1)          // Вращение по часовой

      object[loop].spin-=0.2f*(float(loop+milliseconds));

 

Затем проверим, равняется ли rot двум. Если rot равняется двум, то мы должны вращать против часовой стрелки. Единственное отличие от кода выше то, что мы увеличиваем значение вращения вместо уменьшения его. Это заставляет объект вращаться в противоположном направлении.

 

    if (object[loop].rot==2)          // Вращение против часовой

      object[loop].spin+=0.2f*(float(loop+milliseconds));

 

Теперь код перемещения. Мы проверяем значение dir, если оно равно 1, мы увеличиваем x позицию объекта на значение 0.012f умноженное на миллисекунды. При этом объект перемещается вправо. Поскольку мы используем миллисекунды, объекты должны перемещаться с той же самой скоростью на всех системах.

 

    if (object[loop].dir==1)          // Смещение вправо

      object[loop].x+=0.012f*float(milliseconds);

 

Если dir равняется 0, объект перемещается влево. Мы перемещаем объект, влево уменьшая x координату объекта на прошедшее время в миллисекундах, умноженное на заранее заданное значение 0.012f.

 

    if (object[loop].dir==0)          // Направление налево

      object[loop].x-=0.012f*float(milliseconds);

 

На сей раз, мы проверяем, равняется ли dir 2. Если это так, то мы увеличиваем y координату объекта. При этом объект двигается вверх по экрану. Имейте в виду, что положительная ось Y направлена вверх экрана, а отрицательная ось Y - вниз. Поэтому увеличение y перемещает объект снизу вверх. Снова используется время.

 

    if (object[loop].dir==2)          // Направление вверх

      object[loop].y+=0.012f*float(milliseconds);

 

И последнее, если dir равняется три, то мы хотим переместить объект вниз экрана. Мы делаем это, увеличивая y координату объекта на прошедшее время. Заметьте, что мы передвигаем объекты вниз медленнее чем, мы вверх. Когда объект падает, наша константа падения 0.0025f. Когда объект поднимается - 0.012f.

 

    if (object[loop].dir==3)          // Направление вниз

      object[loop].y-=0.0025f*float(milliseconds);

 

После перемещения наших объектов мы должны проверить попадают ли они еще в поле зрения. Код ниже проверяет, где наш объект находится на экране. Мы можем, приближено вычислить, как далеко сдвинулся объект влево, взяв расстояние объекта от экрана минус 15.0f (чтобы быть уверенными, что это небольшой выход за экран) и разделив это на 2. Для тех, кто не знает... Если Вы сдвинулись на 20 единиц в экран, в зависимости от способа, которым Вы задаете перспективу, Вы имеете примерно 10 единиц в левой части экрана и 10 в правой части. Поэтому -20.0f (расстояние)-15.0f (дополнительно) =-35.0f... делим это на 2, и получаем -17.5f. Это примерно на 7.5 единиц за левым краем экрана. Что означает, что наш объект полностью вне поля зрения.

Так или иначе... После проверки, как далеко объект за левой стороной экрана, мы проверяем, двигается ли он влево (dir=0). Если он не перемещается влево, то нам не надо заботимся о том, что он за левым краем экрана!

Наконец, мы проверяем, было ли попадание в объект. Если объект за экраном, он перемещается влево, и в него не попали, то в этом случае игрок в него уже никогда не попадет. Поэтому мы увеличиваем значение miss. При этом понижаем боевой дух и увеличивает число не сбитых объектов. Мы задаем значение hit объекта в ИСТИНА, поэтому компьютер будет думать, что в этот объект попали. Это вынуждает объект самоуничтожится (позволить нам присвоить объекту новую текстуру, направления, вращение, и т.д).

 

    // Если мы за левым краем, направление влево и в объект не попали

    if ((object[loop].x<(object[loop].distance-15.0f)/2.0f) &&

        (object[loop].dir==0) && !object[loop].hit)

    {

      miss+=1;               // Увеличиваем miss (Промазали по объекту)

      object[loop].hit=TRUE; // Задать hit в True вручную убив объект

    }

 

Следующий код делает тоже самое как и код выше, но проверяет вышел ли объект за правый край экрана. Мы также проверяем перемещается ли объект вправо, а не в другом направлении. Если объект за экраном, мы увеличиваем значение miss и вынуждаем объект самоуничтожится, сообщая нашей программе, что в него попали.

 

    // Если мы за правым краем, направление вправо и в объект не попали

    if ((object[loop].x>-(object[loop].distance-15.0f)/2.0f) &&

        (object[loop].dir==1) && !object[loop].hit)

    {

      miss+=1;               // Увеличиваем miss (Промазали по объекту)

      object[loop].hit=TRUE; // Задать hit в True вручную убив объект

    }

 

Код падения довольно прост. Мы проверяем, попал ли объект в землю. Мы не хотим, чтобы объект провалился сквозь землю, которая расположена на отметке -3.0f. Вместо этого, мы проверяем, находится ли объект ниже -2.0f. Затем проверяем, что объект действительно падает (dir=3), и что в объект еще не попали. Если объект ниже -2.0f по оси Y, мы увеличиваем miss и задаем значение hit объекта в ИСТИНА (вынуждая объект самоуничтожиться, поскольку он упал на землю)... неплохой трюк.

 

    // Если мы за нижним краем, направление вниз и в объект не попали

    if ((object[loop].y<-2.0f) && (object[loop].dir==3) && !object[loop].hit)

    {

      miss+=1;               // Увеличиваем miss (Промазали по объекту)

      object[loop].hit=TRUE; // Задать hit в True вручную убив объект

    }

 

В отличие от предыдущего кода, этот код немного отличается. Мы не хотим, чтобы объект улетел за облака! Мы проверяем больше ли переменная y объекта, чем 4.5f (близко к облакам). Мы также проверяем, что объект двигается вверх (dir=2). Если значение переменной y объекта больше, чем 4.5f, вместо разрушения объекта, мы изменяем его направление. Таким образом, объект быстро упадет на землю (вспомните, что вверх быстрее, чем вниз) и как только он долетит до потолка, мы изменяем его направление, поэтому он начинает падать на землю.

Нет необходимости уничтожать объект, или увеличивать переменную miss. Если Вы промазали в объект, поскольку он улетел в небо, есть всегда шанс, чтобы попасть в него, поскольку он падает. Код падения обработает финальное разрушение объекта.

 

    // Если мы за верхним краем и направление вверх

    if ((object[loop].y>4.5f) && (object[loop].dir==2))

      object[loop].dir=3;          // Изменим на направление вверх

  }

}

 

Следующим идет код рисования объекта. Я хотел иметь наиболее быстрый и простой способ вывода игровых объектов, вместе с перекрестьем, и обойтись при этом небольшим кодом насколько это возможно. Функции Object надо указать 3 параметра. Вначале ширину, она задает насколько будет широк объект, когда он будет выведен. Затем высота, она задает насколько будет высоким объект, когда он будет выведен. Наконец, мы имеем texid. Переменная texid выбирает текстуру, которую мы хотим использовать. Если мы хотим вывести ведро, которое задается текстурой 1, мы передаем значение 1 для texid. Довольно просто!

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

 

// Отрисовка объекта используя требуемые ширину, высоту и текстуру

void Object(float width,float height,GLuint texid)

{

  glBindTexture(GL_TEXTURE_2D, textures[texid].texID); // Выбор правильной текстуры

  glBegin(GL_QUADS); // Начала рисования четырехугольника

    glTexCoord2f(0.0f,0.0f); glVertex3f(-width,-height,0.0f);  // Лево Низ

    glTexCoord2f(1.0f,0.0f); glVertex3f( width,-height,0.0f);  // Право Низ

    glTexCoord2f(1.0f,1.0f); glVertex3f( width, height,0.0f);  // Право Верх

    glTexCoord2f(0.0f,1.0f); glVertex3f(-width, height,0.0f);  // Лево Верх

  glEnd();           // Конец рисования четырехугольника

}

 

Функции отрисовки взрыва Explosion надо указать один параметр. Переменная num – идентификатор объекта. Для создания корректной отрисовки взрыва, мы должны захватить часть текстуры взрыва, таким же способом, каким мы захватываем каждый символ из текстуры шрифта. В двух строках ниже вычисляются столбец (ex) и строка (ey) из одного числа (frame).

В первой строке берется текущий кадр и делится на 4. Деление на 4 должно замедлить мультипликацию. Выражение %4 сохраняет значение в диапазоне 0-3. Если бы значение будет выше, чем 3, то они вернутся и начнутся снова с 0. Если бы значение было 5, то оно станет 1. Значения от 0 до 9 становятся значениями 0,1,2,3,0,1,2,3,0. Мы делим конечный результат 4.0f, потому что координаты текстуры находятся в диапазоне от 0.0f до 1.0f. Наша текстура взрыва имеет 4 изображения взрыва слева направо и 4 сверху вниз.

Надеюсь, что вы не растерялись. Поэтому, если наше число до деления может только быть 0,1,2 или 3, наше число после деления на 4.0f может только быть 0.0f, 0.25f (1/4), 0.50f (2/4) или 0.75f (3/4). Это дает нам нашу слева направо координату текстуры (ex).

Затем мы вычисляем строку (ey). Мы берем текущий кадр объекта и делим его на 4, чтобы немного замедлить мультипликацию. Затем мы делим на 4 снова, чтобы исключить полную строку. Наконец мы делим на 4 последний раз, чтобы получить нашу вертикальную координату текстуры.

Небольшой пример. Если наш текущий кадр равен 16. Тогда ey = ((16/4)/4)/4 или 4/4/4 или 0.25f. Одна строка вниз. Если наш текущий кадр равен 60. Тогда ey = ((60/4)/4)/4 или 15/4/4 или 3/4 или 0.75f. Причина, по которой 15/4 - не равно 3.75 та, что мы работаем с целыми числами вплоть до того, как мы делаем последнее деление. Удерживая это в памяти, мы можем сказать, что значение ey может только быть одним из 4 значений: 0.0f, 0.25f, 0.50f или 0.75f. Принимая это мы остаемся в пределах нашей текстуры (предотвращая превышения текущего кадра выше значения 63).

Надеюсь, это имеет смысл... Это - простая, но наводящая ужас математика.

 

void Explosion(int num)                // Нарисовать анимированный взрыв для объекта "num"

{

  float ex = (float)((object[num].frame/4)%4)/4.0f; // Вычислить X (0.0f - 0.75f)

  float ey = (float)((object[num].frame/4)/4)/4.0f; // Вычислить Y (0.0f - 0.75f)

 

Теперь, когда мы вычислили координаты текстуры, все, что нам осталось нарисовать наш текстурированный четырехугольник. Координаты вершины фиксированы от -1.0f до 1.0f. Вы видите, что мы вычитаем ey из 1.0f. Если бы мы этого не делали, анимация была бы в обратном порядке ... Взрыв должен быть большим, а потом постепенно исчезать. Эффект должен быть правильным!

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

 

  glBindTexture(GL_TEXTURE_2D, textures[5].texID);  // Выбор текстуры взрыва

  glBegin(GL_QUADS);      // Нарисовать четырехугольник

    glTexCoord2f(ex      ,1.0f-(ey      )); glVertex3f(-1.0f,-1.0f,0.0f);  // Лево Низ

    glTexCoord2f(ex+0.25f,1.0f-(ey      )); glVertex3f( 1.0f,-1.0f,0.0f);  // Право Низ

    glTexCoord2f(ex+0.25f,1.0f-(ey+0.25f)); glVertex3f( 1.0f, 1.0f,0.0f);  // Право Верх

    glTexCoord2f(ex      ,1.0f-(ey+0.25f)); glVertex3f(-1.0f, 1.0f,0.0f);  // Лево Верх

  glEnd();                // Четырехугольник нарисован

 

Ранее я упомянул о том, что значение кадра не должно быть больше 63, иначе анимация повторится. Поэтому мы увеличиваем значение кадра, и проверяем его большее ли оно, чем 63. Если это так, то мы вызываем InitObject(num), который уничтожает объект и дает ему новые значения, чтобы создать полностью новый объект.

 

  object[num].frame+=1;         // Увеличим текущий кадр взрыва

  if (object[num].frame>63)     // Отрисованы все 16 кадров?

  {

    InitObject(num);            // Инициализируем объект (назначим новые значения)

  }

}

 

В этой часть кода выводятся все цели на экран. Мы начинаем со сброса матрицы вида модели. Затем мы сдвигаемся на 10 единиц в экран и запускаем цикл от 0 до текущего уровня игрока.

 

void DrawTargets(void)                  // Нарисуем цель

{

  glLoadIdentity();                     // Сброс матрицы вида модели

  glTranslatef(0.0f,0.0f,-10.0f);       // Переход на 10 единиц в экран

  for (int loop=0; loop<level; loop++)  // Цикл по объектам

  {

 

Первой строке кода происходит назначение имени (номера) на каждый объект. Первый выведенный объект будет 0. Второй объект будет 1, и т.д ... Если бы цикл продолжался до 29, последнему выведенному объекту дали бы имя 29. После назначения имени объекту, мы помещаем матрицу вида модели в стек. Важно обратить внимание на то, что вызовы glLoadName() игнорируются, если программа не в режиме выбора.

Затем мы перемещаемся на то место на экране, где мы хотим, чтобы наш объект был выведен. Мы используем object[loop].x, чтобы позиционировать объект по оси X, object[loop].y чтобы позиционировать объект по оси Y и object[loop].distance, чтобы позиционировать объект по оси Z (глубина). Мы уже переместились на 10 единиц в экран, поэтому фактическое расстояние, на которое объект будет выведен, равно object[loop].distance-10.0f.

 

    glLoadName(loop);            // Назначение имени объекта (ID)

    glPushMatrix();              // Поместить в стек матрицу

                                 // Позиционирование объекта (x,y)

    glTranslatef(object[loop].x,object[loop].y,object[loop].distance);

 

До того как мы выведем объект, мы должны проверить попали ли в него или нет. Мы делаем это, проверяя равно ли значение object[loop].hit ИСТИНА. Если это так, то мы вызываем Explosion(loop), которая анимирует взрыв вместо фактического объекта. Если в объект не попали, мы вращаем объект по оси Z на object[loop].spin градусов до того как мы вызовем Object(…).

В функцию Object() передаются 3 параметра. Первый - ширина, второй - высота, и третий - номер текстуры. Чтобы получить ширину и высоту, мы используем массив size[object[loop].texid].w и size[object[loop].texid].h. Так мы найдем ширину и высоту из нашего заранее определенного в начале этой программы массива размеров объектов. Причина, по которой мы используем object[loop].texid состоит в том, что texid указывает на тип объекта, который мы выводим. Если texid равно 0, то это всегда голубая рожа... texid равно 3, то это всегда кока-кола, и т.д.

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

 

    if (object[loop].hit)       // В объект попали

    {

      Explosion(loop);          // Нарисовать взрыв

    }

    else                        // Иначе

    {

                                // Вращать объект

      glRotatef(object[loop].spin,0.0f,0.0f,1.0f);

                                // нарисовать объект

      Object(size[object[loop].texid].w,size[object[loop].texid].h,object[loop].texid);

    }

    glPopMatrix();              // Вытолкнуть матрицу

  }

}

 

Далее то место, где происходит рисование. Мы начинаем с очистки экран, и сбрасывая матрицу вида модели.

 

void Draw(void)                  // Нарисовать нашу сцену

{

                                 // Очистка экрана и буфера глубины

  glClear (GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

  glLoadIdentity();              // Сброс матрицы просмотра вида

 

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

 

  glPushMatrix();                // Запомнить матрицу

  glBindTexture(GL_TEXTURE_2D, textures[7].texID); // Выбор текстуры неба

  glBegin(GL_QUADS);              // Начало рисования четырехугольников

    glTexCoord2f(1.0f,roll/1.5f+1.0f); glVertex3f( 28.0f,+7.0f,-50.0f);  // Право Верх

    glTexCoord2f(0.0f,roll/1.5f+1.0f); glVertex3f(-28.0f,+7.0f,-50.0f);  // Лево Верх

    glTexCoord2f(0.0f,roll/1.5f+0.0f); glVertex3f(-28.0f,-3.0f,-50.0f);  // Лево Низ

    glTexCoord2f(1.0f,roll/1.5f+0.0f); glVertex3f( 28.0f,-3.0f,-50.0f);  // Право Низ

 

    glTexCoord2f(1.5f,roll+1.0f); glVertex3f( 28.0f,+7.0f,-50.0f);    // Право Верх

    glTexCoord2f(0.5f,roll+1.0f); glVertex3f(-28.0f,+7.0f,-50.0f);    // Лево Верх

    glTexCoord2f(0.5f,roll+0.0f); glVertex3f(-28.0f,-3.0f,-50.0f);    // Лево Низ

    glTexCoord2f(1.5f,roll+0.0f); glVertex3f( 28.0f,-3.0f,-50.0f);    // Право Низ

 

Чтобы создать иллюзию, что небо движется к зрителю, мы выводим еще два четырехугольника, но на сей раз мы рисуем их в верхней полоске неба. Первые 4 вершины выводят медленно движущиеся облака, и оставшиеся 4 выводит более быстро перемещающиеся облака. Эти два уровня смешаются вместе в режиме альфа смешивания, чтобы создать слоистый эффект. Второй уровень облаков сдвинут на 0.5f так, чтобы две текстуры не совпадали друг с другом. Тоже самое с двумя слоями облаков выше. Второй слой смещен на 0.5f.

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

 

    glTexCoord2f(1.0f,roll/1.5f+1.0f); glVertex3f( 28.0f,+7.0f,0.0f);  // Право Верх

    glTexCoord2f(0.0f,roll/1.5f+1.0f); glVertex3f(-28.0f,+7.0f,0.0f);  // Лево Верх

    glTexCoord2f(0.0f,roll/1.5f+0.0f); glVertex3f(-28.0f,+7.0f,-50.0f);  // Лево Низ

    glTexCoord2f(1.0f,roll/1.5f+0.0f); glVertex3f( 28.0f,+7.0f,-50.0f);  // Право Низ

 

    glTexCoord2f(1.5f,roll+1.0f); glVertex3f( 28.0f,+7.0f,0.0f);    // Право Верх

    glTexCoord2f(0.5f,roll+1.0f); glVertex3f(-28.0f,+7.0f,0.0f);    // Лево Верх

    glTexCoord2f(0.5f,roll+0.0f); glVertex3f(-28.0f,+7.0f,-50.0f);    // Лево Низ

    glTexCoord2f(1.5f,roll+0.0f); glVertex3f( 28.0f,+7.0f,-50.0f);    // Право Низ

  glEnd();                // Кончили рисовать

 

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

Текстура повторяется 7 раз слева направо и 4 раза от горизонта до зрителя для увеличения детализации и предотвращения появления на текстуре больших блочных пикселей (blocky). Это делается при помощи увеличения диапазона координат текстуры от 0.0f - 1.0f до 0.0f - 7.0f (слева направо) и 0.0f - 4.0f (вверх и вниз).

 

  glBindTexture(GL_TEXTURE_2D, textures[6].texID); // Выбор текстуры земли

  glBegin(GL_QUADS);      // Начало рисования четырехугольника

    glTexCoord2f(7.0f,4.0f-roll); glVertex3f( 27.0f,-3.0f,-50.0f);  // Право Верх

    glTexCoord2f(0.0f,4.0f-roll); glVertex3f(-27.0f,-3.0f,-50.0f);  // Лево Верх

    glTexCoord2f(0.0f,0.0f-roll); glVertex3f(-27.0f,-3.0f,0.0f);  // Лево Низ

    glTexCoord2f(7.0f,0.0f-roll); glVertex3f( 27.0f,-3.0f,0.0f);  // Право Низ

  glEnd();                // Кончили рисовать

 

После того как мы нарисовали небо и землю, мы переходим к разделу кода, который выводит все наши цели в DrawTargets().

После вывода целей, мы возвращаем матрицу вида модели (восстанавливая ее к предыдущему состоянию).

 

  DrawTargets();                // Нарисовать наши цели

  glPopMatrix();                // Возврат матрицы

 

Следующий код выводит перекрестье. Вначале мы получаем текущие размеры нашего окна. Мы делаем это каждый раз, так размеры окна могут быть изменены в оконном режиме. Функция GetClientRect возвращает размеры и сохраняет их в window. Затем мы выбираем нашу матрицу проецирования и помещаем ее в стек. Мы сбрасываем вид вызовом glLoadIdentity() и затем переключаем экран режим ортографической проекции вместо перспективной. Окно начинается с 0 до window.right слева направо, и от 0 до window.bottom с нижнего края экрана до верхнего.

Третий параметр glOrtho() задает значение нижнего края, но я поменял значения нижнего и верхнего края местами. Я сделал это, потому что перекрестье выводится в направлении против часовой стрелки. Если 0 будет сверху, а window.bottom в низу, обход вершин при отображении будет в противоположном направлении, и перекрестье с текстом не будут отображаться.

После настройки ортографического вида, мы выбираем матрицу вида модели, и устанавливаем перекрестье. Поскольку экран перевернут снизу верх, мы должны также инвертировать мышь. Иначе наше перекрестье двигалось бы вниз, если бы мы переместили бы мышь верх, и наше перекрестье двигалось бы верх, если бы мы переместили бы мышь вниз. Чтобы сделать это, мы вычитаем текущее значение mouse_y из значения нижнего края окна (window.bottom).

После переноса в текущую позицию мыши, мы выводим перекрестье. Мы делаем это, вызывая Object(). Вместо единиц, мы задаем ширину и высоту в пикселях. Перекрестье будет размером 16x16 пикселей, и используется текстура от восьмого объекта (текстура перекрестья).

Я решил использовать не стандартный курсор по двум причинам. Первая причина и наиболее важная состоит в том, что это выглядит привлекательно, и курсор можно изменить, используя любую программу рисования, которая поддерживает альфа канал. Во-вторых, некоторые видео платы не отображают курсор в полноэкранном режиме. Играть без курсора в полноэкранном режиме не просто :).

 

  // Перекрестье (Ортографический просмотр)

  RECT window;                   // Размеры окна

  GetClientRect (g_window->hWnd,&window); // Получить их

  glMatrixMode(GL_PROJECTION);            // Выбор матрицы проекции

  glPushMatrix();                // Сохранить матрицу

  glLoadIdentity();              // Сброс матрицы

  glOrtho(0,window.right,0,window.bottom,-1,1);     // Настройка ортографического экрана

  glMatrixMode(GL_MODELVIEW);    // Выбор матрицы вида модели

  glTranslated(mouse_x,window.bottom-mouse_y,0.0f); // Перенос в текущую позицию мыши

  Object(16,16,8);               // Нарисовать перекрестье

 

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

 

  // Счет и Заголовок игры

  glPrint(240,450,"NeHe Productions"); // Заголовок

  glPrint(10,10,"Level: %i",level);    // Уровень

  glPrint(250,10,"Score: %i",score);   // Счет

 

В этом разделе проверяется, пропустил ли игрок больше чем 10 объектов. Если так, то мы устанавливаем число промахов (miss) в 9, и мы задаем game в ИСТИНА. Если game равно ИСТИНА, то игра закончена!

 

  if (miss>9)              // Пропустил 10 объектов?

  {

    miss=9;                // Предел равен 10

    game=TRUE;             // Конец игры

  }

 

В коде ниже, мы проверяем, является ли game ИСТИННА. Если game ИСТИННА, мы выводим сообщение - ИГРА ОКОНЧЕНА. Если game ЛОЖЬ, мы выводим боевой дух игрока (до 10). Боевой дух вычисляется, вычитая число промахов игрока (miss) из 10. Чем больше промахов, тем более низкий у него боевой дух.

 

  if (game)                // Игра окончена?

    glPrint(490,10,"GAME OVER"); // Сообщение о том, что игра окончена

  else

    glPrint(490,10,"Morale: %i/10",10-miss); // Вывод морали #/10

 

Последнее, что мы делаем выбор матрицы проецирования, восстановление нашей матрицы в предыдущее состояние, выбор матрицы вида модели и сброс буфера, чтобы быть уверенным, что все объекты выведены.

 

  glMatrixMode(GL_PROJECTION);  // Выбор матрицы проецирования

  glPopMatrix();                // Восстановление старой матрицы

  glMatrixMode(GL_MODELVIEW);   // Выбор матрицы вида модели

 

  glFlush();                    // Сброс конвейера GL

}

 

Этот урок появился после многих ночей, и многих многих часов кодирования и редактирования HTML. По окончанию этого урока Вы должны хорошо понимать, как работает выбор, сортировка, альфа смешивание и альфа тест. Выбор позволяет Вам создавать процедуры для позиционирования и выбора объектов. Любая игра – это иллюзия графического интерфейса. Лучшая возможность выбора заключается в том, что Вы не должны следить за тем, где ваши объекты. Вы назначаете имя и проверяете попадания. Это просто! Используя альфа смешивание и альфа тест Вы может делать ваши объекты полностью непрозрачными, или полностью прозрачными только в отдельных местах. Результаты великолепны, и Вы не должны волноваться о просвечивании через ваши объекты фона за ним, если Вы не хотите этого! Как всегда, я буду надеяться, что Вы получите наслаждение от этого урока, и надеюсь увидеть новые крутые игры, или проекты, основанные на коде этого урока. Если у Вас есть вопросы, или Вы нашли ошибки в уроке, пожалуйста, сообщите мне об этом ... Я всего лишь человек :).

Я мог потратить намного больше времени, добавив, например физику, или улучшив графику, или звук, и т.д. Но это только урок! И я не писал этот урок так, чтобы поразить вас. Я написал его, чтобы обучить Вас OpenGL с наименьшими отклонениями от темы насколько это возможно. Я надеюсь увидеть модификации кода. Если Вы добавите что-то в урок, то вышлите мне демонстрационную версию своей программы. Если это великолепная модификация, я помещу ее на страницах "Download". Если я получу достаточно модификаций, я могу изменить этот урок! Я должен здесь дать Вам точку опоры. Все остальное за Вами :) .

© Jeff Molofee (NeHe)

PMG  22 апреля 2003 (c)  Сергей Анисимов