Мониторинг обменных курсов валют
18c0693f

Урок 21. Линии, сглаживание, синхронизация,


u="u013.94.spylog.com";d=document;nv=navigator;na=nv.appName;p=1; bv=Math.round(parseFloat(nv.appVersion)*100); n=(na.substring(0,2)=="Mi")?0:1;rn=Math.random();z="p="+p+"&rn="+rn+"&tl=0&ls=0&ln=0";y=""; y+="
"; d.write(y);if(!n) { d.write("



Народный учебник по OpenGL
Урок 21. Линии, сглаживание, синхронизация, ортографическая проекция и звуки.

Lines, Antialiasing, Timing, Ortho View And Simple Sounds



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

И все же, если я потерял кое-кого из Вас :), то я хочу рассказать Вам немного об этом уроке. До сих пор во всех моих уроках использовались многоугольники, четырехугольники и треугольники. Поэтому я решил, что будет интересно создать урок о линиях. После нескольких часов создания урока о линиях, я решил не продолжать его. Урок вышел отличным, но он был СКУЧНЫМ! Линии, конечно, это замечательно, но надо сделать что-то невероятное, чтобы линии стали интересными. Тогда я просмотрел ваши письма на форуме, и запомнил несколько ваших просьб. Из них было несколько вопросов, которые подошли больше чем другие. Итак... Я решил написать мультиурок :).

В этом уроке Вы научитесь: выводить линии, делать сглаживание, оперировать ортографической проекцией, осуществлять синхронизацию времени, выводить простейшие звуковые эффекты, и реализовывать простую игровую логику. Буду надеяться, что в этом уроке есть все, чтобы сделать каждого счастливым :). Я потратил 2 дня, чтобы создать программу урока, и почти 2 недели, чтобы написать текст урока (и потребовалось 3 месяца, чтобы перевести это урок). Я надеюсь, что Вы будете наслаждаться моим трудом!




По окончанию этого урока у Вас будет простая готовая игра типа 'amidar'. Ваша миссия состоит в том, чтобы закрасить сетку и не попасться при этом в “лапы” плохими парнями. Игра имеет уровни, стадии, жизни, звук, и секретный предмет, чтобы помочь Вам переходить с уровня на уровень, когда ситуация становится критичной. Хотя эта игра прекрасно работает на Pentium 166 с Voodoo 2, более быстрый процессор рекомендуется, если Вы хотите иметь более плавную анимацию.

Я использовал код урока 1 в качестве отправной точки при написании этого урока. Мы начинаем с того, что включаем необходимые заголовочные файлы. Файл stdio.h используется для операций с файлами, и мы включаем stdarg.h для того чтобы мы могли вывести переменные на экран, типа счета и текущей стадии.

// Этот код сделан Jeff Molofee в 2000

// Если вы сочли этот код полезным, то дайте мне знать.



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

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

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

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

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

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



HDC       hDC=NULL;  // Частный контекст устройства GDI

HGLRC     hRC=NULL;  // Контекст текущей визуализации

HWND      hWnd=NULL; // Декриптор нашего окна

HINSTANCE hInstance; // Копия нашего приложения



Теперь мы задаем наши булевские переменные. Переменная vline отслеживает все 121 вертикальную линию, которые составляют нашу игровую сетку. 11 линий вдоль и 11 вверх и вниз. Переменная hline отслеживает все 121 горизонтальную линию, которые составляют игровую сетку. Мы используем переменную ap для отслеживания, действительно ли клавиша “A” нажата.

Значение переменной filled равно ЛОЖЬ, пока сетка не закрашена и равно ИСТИНА, когда она закрашена. Назначение переменной gameover довольно очевидно. Если gameover равно ИСТИНА, то игра закончена, иначе Вы все еще играете. Переменная anti отслеживает сглаживание (antialiasing). Если anti равно ИСТИНА, сглаживание объектов ВКЛЮЧЕНО. Иначе оно выключено. Переменные active и fullscreen отслеживают, была ли программа свернута или нет, и запущена программа в полноэкранном режиме или оконном режиме.





bool    vline[11][10];     // Отслеживает вертикальные линии

bool    hline[10][11];     // Отслеживает горизонтальные линии

bool    ap;                // Клавиша 'A' нажата?

bool    filled;            // Сетка закрашена?

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

bool    anti=TRUE;         // Сглаживание?

bool    keys[256];         // Массив для манипуляций с клавиатурой

bool    active=TRUE;       // Флаг активности окна, по умолчанию=TRUE

bool    fullscreen=TRUE;   // Флаг полноэкранного режима, по умолчанию=TRUE



Теперь мы определяем наши целые переменные. Переменные loop1 и loop2 будут использоваться для разных целей, например: для проверки точек на нашей сетке, для проверки попадания противника в нас и для случайного размещения объектов на сетке. Вы увидите loop1/loop2 в действии позже. Переменная-счетчик delay используется, чтобы замедлить перемещение плохих парней. Если delay больше чем некоторое значение, враги двигаются, и delay сбрасывается в ноль.



Переменная adjust - особенная переменная! В нашей программе есть таймер, но он используется только для проверки, если ваш компьютер слишком быстр. Если это так, то delay создана, чтобы замедлить компьютер. На моей плате GeForce, программа выполняется безумно гладко, и очень быстро. После проверки этой программы на моем PIII/450 с Voodoo 3500TV, я заметил, что она выполняется чрезвычайно медленно. Проблема состоит в том, что мой код синхронизации, только замедляет игру. Но не ускоряет ее. Поэтому я ввел новую переменную, называемую adjust (коррекция). Переменная adjust может принимать любое значение от 0 до 5. Объекты в игре перемещаются с различными скоростями в зависимости от значения adjust. Маленькие значения задают более гладкое перемещение, чем выше значение, тем они быстрее двигаются (граница после значений выше, чем 3). Это был единственно действительно простой способ сделать игру выполняемой на медленных системах. На одну вещь обратите внимание, независимо от того, как быстро объекты перемещаются, быстродействие игры никогда не будет больше чем, я ее назначил. Так присваивание переменной adjust значения равного 3, безопасно для быстрых и медленных систем.





Переменной lives присвоено значение 5, поэтому Вы начинаете игру с 5 жизнями. Переменная level - внутренняя переменная. В игре она используется, для того чтобы отслеживать уровень сложности. Это не тот уровень, который Вы увидите на экране. Переменной level2 присваивается то же самое значение, как и level, но ее значение необратимо увеличивается в зависимости от вашего навыка. Если Вы прошли уровень 3, переменная level замрет на значении 3. Переменная level - внутренняя переменная, характеризующая сложность игры. Переменная stage отслеживает текущую стадию игры.



int    loop1;              // Общая переменная 1

int    loop2;              // Общая переменная 2

int    delay;              // Задержка для Противника

int    adjust=3;           // Настройка скорости для медленных видеокарт

int    lives=5;            // Жизни для игрока

int    level=1;            // Внутренний уровень игры

int    level2=level;       // Уровень игры для отображения

int    stage=1;            // Стадия игры



Теперь мы создадим структуру для слежения за объектами в нашей игре. Мы имеем точное положение по X (fx) и точное положение по Y (fy). Эти переменные будут передвигать игрока и противников по сетки сразу на несколько пикселей. Они служат для создания плавного перемещения объекта.



Затем мы имеем x и y. Эти переменные будут отслеживать, в каком узле сетки находится наш игрок. Есть 11 точек слева направо и 11 точек сверху вниз. Поэтому переменные x и y могут принимать любое значение от 0 до 10. Именно поэтому мы нуждаемся в точных значениях. Если бы мы стали перемещать игрока с одного из 11 узлов по горизонтали, или с одного из 11 узлов по вертикали на другой соседний узел, то наш игрок быстро бы прыгал по экрану, а не плавно двигался между ними.





Последняя переменная spin будет использоваться для вращения объектов относительно оси Z.



struct    object           // Структура для игрока

{

  int  fx, fy;             // Точная позиция для передвижения

  int  x, y;               // Текущая позиция игрока

  float  spin;             // Направление вращения

};



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



В первой строке ниже создается структура для нашего игрока. По существу мы даем нашему игроку структуру со значениями fx, fy, x, y и spin. Добавив эту строку, мы сможем обратиться к позиции игрока x при помощи записи player.x. Мы можем изменять вращение игрока, добавляя число к player.spin.



Вторая строка немного отличается. Поскольку мы можем иметь до 9 противников на экране одновременно, мы должны создать вышеупомянутые переменные для каждого противника. Мы делаем для этого массив из 9 противников. Позиция x первого противника будет enemy[0].x. Позиция второго противника будет enemy[1].x, и т.д.



Последняя строка создает структуру для нашего специального элемента. Специальный элемент - песочные часы, которые будут появляться на экране время от времени. Мы должны следить за значениями x и y песочных часов, но так как песочные часы не двигаются, мы не должны следить за точными позициями. Вместо этого мы будем использовать точные переменные (fx и fy) для других целей.



struct  object  player;                // Информация о игроке

struct  object  enemy[9];              // Информация о противнике



struct  object  hourglass;             // Информация о песочных часах



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



Вначале мы создаем целое число размером 64 бита, которое называется frequency (частота). В эту переменную будет помещено значение частоты таймера. Когда я вначале написал эту программу, я забыл включить эту переменную. Я не понимал то, что частота на одной машине не может соответствовать частоте на другой. Моя большая ошибка! Код выполнился прекрасно на 3 системах в моем доме, но когда я проверил его на машине моих друзей, игра работала ОЧЕНЬ быстро. Частота – говорит о том, как быстро часы обновляются. Хорошая вещь для слежки :).



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



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



Переменная performance_timer может быть равной или ИСТИНА или ЛОЖЬ. Если программа находит высокоточный таймер, переменная performance_timer будет равна ИСТИНА, и синхронизация использует высокоточный таймер (намного более точный, чем мультимедийный таймер). Если высокоточный таймер не найден, переменная performance_timer будет равна ЛОЖЬ и мультимедийный таймер используется для синхронизации.



Последние 2 переменные - целые переменные по 64 бита, которые содержат время запуска высокоточного таймера и время, которое прошло с момента запуска высокоточного таймера.



Название этой структуры - " timer", как Вы можете увидеть внизу структуры. Если мы хотим знать частоту таймера, мы можем теперь проверить timer.frequency. Отлично!





struct                    // Создание структуры для информации о таймере

{

  __int64       frequency;                 // Частота таймера

  float         resolution;                // Точность таймера

  unsigned long mm_timer_start;            // Стартовое значение мультимедийного таймера

  unsigned long mm_timer_elapsed;          // Прошедшее время мультимедийного таймера

  bool          performance_timer;         // Использовать высокоточный таймер?

  __int64       performance_timer_start;   // Стартовое значение высокоточного таймера

  __int64       performance_timer_elapsed; // Прошедшее время высокоточного таймера

} timer;                  // Структура по имени таймер



Следующая строка кода - наша таблица скорости. Объекты в игре будут двигаться с разной скоростью в зависимости от значения adjust (коррекции). Если adjust - 0, объекты будут перемещаться на один пиксель одновременно. Если значение adjust - 5, объекты переместят на 20 пикселей одновременно. Таким образом, увеличивая значение adjust, скорость объектов увеличится, делая выполнение игры более быстрой на медленных компьютерах. Однако при больших значениях adjust игра будет выглядеть более дерганой.



Массив steps[] - только таблица для поиска. Если adjust равно 3, мы ищем число в позиции 3 массива steps[]. В позиции 0 хранится значение 1, в позиции 1 хранится значение 2, в позиции 2 хранится значение 4, и в позиции 3 хранится значение 5. Если adjust равно 3, наши объекты перемещались бы на 5 пикселей одновременно. Понятен смысл?





int    steps[6]={ 1, 2, 4, 5, 10, 20 }; // Значения шагов для работы

                                        // на медленных видеокартах



Затем мы создаем память для двух текстур. Мы загрузим фон сцены, и картинку для шрифта. Затем мы определяем переменную base, для списка отображения шрифта точно так же как, мы делали в других уроках со шрифтами. Наконец мы объявляем WndProc().



GLuint    texture[2];          // Память для текстур

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



LRESULT  CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); // Объявление для WndProc



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



Вначале сбросим все переменные структуры таймера в ноль. Это присвоит всем переменным в нашей структуре таймера значение ноль. После этого, мы проверим наличие высокоточного таймера. Здесь знак '!' означает НЕТ. Если таймер есть, то частота будет сохранена в timer.frequency.



Если нет высокоточного таймера, код между скобками будет выполнен. В первой строке переменной performance_timer присваивается ЛОЖЬ. Это говорит нашей программе, что нет никакого высокоточного счетчика. Во второй строке мы получаем стартовое значение для мультимедийного таймера от timeGetTime(). Мы задаем timer.resolution в 0.001f, и timer.frequency к 1000. Поскольку еще не прошло время, мы присваиваем прошедшему времени время запуска.



void TimerInit(void) // Инициализация нашего таймера (Начали)



{

  memset(&timer, 0, sizeof(timer)); // Очистка нашей структуры



  // Проверим доступность высокоточного таймера

  // Если доступен, то частота таймера будет задана

  if (!QueryPerformanceFrequency((LARGE_INTEGER *) &timer.frequency))

  {

    // Нет высокоточного таймера

    timer.performance_timer  = FALSE;       // Установим флаг высокоточного таймера в ЛОЖЬ

    timer.mm_timer_start  = timeGetTime();  // Текущее время из timeGetTime()

    timer.resolution  = 1.0f/1000.0f;       // Точность равна 0.001f

    timer.frequency    = 1000;              // Частота равна 1000

    timer.mm_timer_elapsed  = timer.mm_timer_start; // Прошедшее время равно текущему

  }



Если есть высокоточный таймер, следующий код будет выполнен вместо этого. В первой строке захватывается значение запуска высокоточного таймера, и помещается в performance_timer_start. Затем мы присваиваем переменной performance_timer значение ИСТИНА так, чтобы наша программа знала, что есть доступный высокоточный таймер. После этого, мы, вычисляем точность таймера, используя частоту, которую мы получили, когда проверяли наличие высокоточного таймера в коде выше. Мы делим единицу на эту частоту, чтобы получить точность. Последнее что мы сделаем, будет присвоение прошедшему времени значения стартового времени.



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



  else

  {

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

    // Взять текущее время и сохранить его в performance_timer_start



    QueryPerformanceCounter((LARGE_INTEGER *) &timer.performance_timer_start);

    timer.performance_timer    = TRUE;        // Установить флаг наличия таймера в TRUE

    // Вычислить точность таймера, используя частоту

    timer.resolution    = (float) (((double)1.0f)/((double)timer.frequency));

    // Присвоить прошедшему времени текущее время

    timer.performance_timer_elapsed  = timer.performance_timer_start;

  }

}



Раздел кода выше инициализирует таймер. Код ниже читает таймер и возвращает время, которое прошло в миллисекундах.



Вначале определим переменную в 64 бита под именем time. Мы будем использовать эту переменную, чтобы получить текущее время. Следующая строка проверяет, доступен ли высокоточный таймер. Если performance_timer равен ИСТИНА, то код после условия выполнится.



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



Если мы не используем высокоточный таймер, код после инструкции else будет выполнен. Там в значительной степени делается тоже самое. Мы захватываем текущее время с помощью timeGetTime() и вычитаем из него наше значение при запуске. Мы умножаем на точность и затем на 1000, чтобы преобразовать результат из секунд в миллисекунды.



float TimerGetTime()           // Взять время в миллисекундах



{

  __int64 time;                // time содержит 64 бита



  if (timer.performance_timer) // Есть высокоточный таймер?

  {

    // Захват текущего значения высокоточного таймера

    QueryPerformanceCounter((LARGE_INTEGER *) &time);

    // Вернем текущее время минус начальное время, умноженное на точность и 1000 (для миллисекунд)

    return ( (float) ( time - timer.performance_timer_start) * timer.resolution)*1000.0f;

  }

  else

  {

    // Вернем текущее время минус начальное время, умноженное на точность и 1000 (для миллисекунд)

    return( (float) ( timeGetTime() - timer.mm_timer_start) * timer.resolution)*1000.0f;

  }

}



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



Левый верхний угол экрана – это 0 по оси X и 0 по оси Y. Поэтому, устанавливая player.x в 0, мы помещаем игрока на левый край экрана. Устанавливая player.y в 0, мы помещаем нашего игрока на верхний край экрана.



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



void ResetObjects(void)      // Сброс Игрока и Противников

{

  player.x=0;                // Сброс позиции игрока X на левый край экрана

  player.y=0;                // Сброс позиции игрока Y на верх экрана

  player.fx=0;               // Установим точную позиции X

  player.fy=0;               // Установим точную позиции Y





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



Чтобы быть уверенными, что мы даем всем видимым противникам новую позицию, мы организуем цикл по всем видимым противникам (стадия, умноженная на уровень). Мы устанавливаем для каждого противника позицию x равную 5 плюс случайное значение от 0 до 5 (максимальное случайное значение может быть всегда число, которое Вы зададите минус 1). Поэтому враги могут появляться на сетке, где-то от 5 до 10. Затем мы даем врагу случайное значение по оси Y от 0 до 10.



Мы не хотим, чтобы враг двигался из старой позиции к новой случайной позиции, поэтому мы должны быть уверены, что точные значения по x (fx) и y (fy) равны значениям по x и y, умноженные на ширину и высоту каждой ячейки на экране. Каждая ячейка имеет ширину 60 и высоту 40.



  for (loop1=0; loop1<(stage*level); loop1++) // Цикл по всем противникам

  {

    enemy[loop1].x=5+rand()%6;                // Выбор случайной позиции X

    enemy[loop1].y=rand()%11;                 // Выбор случайной позиции Y

    enemy[loop1].fx=enemy[loop1].x*60;        // Установка точной X

    enemy[loop1].fy=enemy[loop1].y*40;        // Установка точной Y

  }

}



Код AUX_RGBImageRec не изменился, поэтому я опускаю его. В LoadGLTextures() мы загрузим две наши текстуры. Сначала картинку шрифта (Font.bmp) и затем фоновое изображение (Image.bmp). Мы конвертируем оба изображения в текстуры, которые мы можем использовать в нашей игре. После того, как мы построили текстуры, мы очищаем память, удаляя растровую информацию. Здесь нет ничего нового. Если Вы читали другие уроки, Вы не должны иметь никаких проблем, в понимании кода.





int LoadGLTextures()                       // Загрузка растра и конвертирование его в текстуры

{

  int Status=FALSE;                        // Индикатор статуса

  AUX_RGBImageRec *TextureImage[2];        // Память для текстур

  memset(TextureImage,0,sizeof(void *)*2); // Указатель в NULL



  if   ((TextureImage[0]=LoadBMP("Data/Font.bmp")) &&  // Загрузка фонта

     (TextureImage[1]=LoadBMP("Data/Image.bmp")))      // Загрузка фона

  {

    Status=TRUE;                           // Установка статуса в TRUE



    glGenTextures(2, &texture[0]);         // Создание текстуры



    for (loop1=0; loop1<2; loop1++)        // Цикл из 2 текстур

    {

      glBindTexture(GL_TEXTURE_2D, texture[loop1]);

      glTexImage2D(GL_TEXTURE_2D, 0, 3, TextureImage[loop1]->sizeX, TextureImage[loop1]->sizeY,

        0, GL_RGB, GL_UNSIGNED_BYTE, TextureImage[loop1]->data);

      glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_LINEAR);

      glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_LINEAR);

    }



    for (loop1=0; loop1<2; loop1++)        // Цикл из 2 текстур

    {

      if (TextureImage[loop1])             // Если текстура существует



      {

        if (TextureImage[loop1]->data)     // Если изображение текстуры существует

        {

          free(TextureImage[loop1]->data); // Освободить память текстуры

        }

        free(TextureImage[loop1]);         // Освободить структуру изображения

      }

    }

  }

  return Status;                           // Возврат статуса

}



Код ниже создает список отображения шрифта. Я уже делал урок со шрифтом из текстуры. Весь код, делит изображение Font.bmp на 16 x 16 ячеек (256 символов). Каждая ячейка размером 16x16 станет символом. Поскольку я задал ось Y направленную вверх, поэтому, чтобы происходил сдвиг вниз, а не вверх, необходимо вычесть наши значения по оси Y из значения 1.0f. Иначе символы будут инвертированны :). Если Вы не понимаете то, что происходит, возвратитесь, и читайте урок по шрифтам из текстур.



GLvoid BuildFont(GLvoid)                     // Создаем список отображения нашего шрифта

{

  base=glGenLists(256);                      // Создаем списки

  glBindTexture(GL_TEXTURE_2D, texture[0]);  // Выбираем текстуру шрифта

  for (loop1=0; loop1<256; loop1++)          // Цикл по всем 256 спискам

  {

    float cx=float(loop1%16)/16.0f;          // X координата текущего символа



    float cy=float(loop1/16)/16.0f;          // Y координата текущего символа



    glNewList(base+loop1,GL_COMPILE);              // Начинаем делать список

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

        glTexCoord2f(cx,1.0f-cy-0.0625f);          // Точка в текстуре (Левая нижняя)

        glVertex2d(0,16);        // Координаты вершины (Левая нижняя)

        glTexCoord2f(cx+0.0625f,1.0f-cy-0.0625f);  // Точка на текстуре (Правая нижняя)

        glVertex2i(16,16);       // Координаты вершины (Правая нижняя)

        glTexCoord2f(cx+0.0625f,1.0f-cy);          // Точка текстуры (Верхняя правая)

        glVertex2i(16,0);        // Координаты вершины (Верхняя правая)

        glTexCoord2f(cx,1.0f-cy);                  // Точка текстуры (Верхняя левая)

        glVertex2i(0,0);         // Координаты вершины (Верхняя левая)

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

      glTranslated(15,0,0);      // Двигаемся вправо от символа

    glEndList();                 // Заканчиваем создавать список отображения



  }                              // Цикл для создания всех 256 символов

}



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



GLvoid KillFont(GLvoid)               // Удаляем шрифт из памяти

{

  glDeleteLists(base,256);            // Удаляем все 256 списков отображения

}



Код glPrint() изменился не значительно. Единственное отличие от урока об текстурных шрифтах то, что я добавил возможность печатать значение переменных. Единственная причина, по которой я привожу этот раздел кода это та, что Вы можете при этом увидеть изменения. Вызов функции печати позиционирует текст в позиции x и y, которые Вы задаете. Вы можете выбрать один из 2 наборов символов, и значение переменных будет выведено на экран. Это позволит нам отображать текущий уровень и стадию.



Заметьте, что я разрешаю наложение текстуры, сбрасываю матрицу вид, и затем уставливаю в необходимую x / y позицию. Также заметьте, что, если выбран набор символов 0, шрифт укрупнен по ширине в полтора раз, и в два раза по высоте от первоначального размера. Я сделал это для того чтобы написать заголовок игры большими буквами. После того, как текст выведен, я отключаю наложение текстуры.



GLvoid glPrint(GLint x, GLint y, int set, const char *fmt, ...) // Печать

{

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

  va_list    ap;              // Ссылка на список аргументов



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

    return;                   // то выходим





  va_start(ap, fmt);          // Разбор строки из значений

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

  va_end(ap);                 // Результат в текст



  if (set>1)                  // Если выбран не верный набор символов?

  {

    set=1;                    // Если так, то выбрать набор 1 (Курсив)

  }

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

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

  glTranslated(x,y,0);        // Позиция текста (0,0 – Низ Лево)

  glListBase(base-32+(128*set)); // Выбор набора символов (0 или 1)



  if (set==0)                 // Если 0 используем укрупненный фонт

  {

    glScalef(1.5f,2.0f,1.0f); // Ширина и Высота укрупненного шрифта

  }



  glCallLists(strlen(text),GL_UNSIGNED_BYTE, text); // Вывод текста на экран

  glDisable(GL_TEXTURE_2D);   // Запрет наложения текстуры

}



Код изменения размеров НОВЫЙ :). Вместо использования перспективной проекции я использую ортографическую проекцию для этого урока. Это означает, что объекты не уменьшаются, когда они удаляются от наблюдателя. Ось Z не используется в этом уроке.



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



Затем мы выбираем матрицу проецирования и сбросим ее.





Сразу же после того, как мы сбрасываем матрицу проецирования, мы устанавливаем ортогональное проецирование. Я объясню эту команду подробнее.



Первый параметр (0.0f) - значение, которое мы хотим иметь на крайней левой стороне экрана. Вы хотели бы узнать, как использовать реальные значения пикселей, а не трехмерные координаты. Поэтому вместо использования отрицательного числа для левого края, я задал значение 0. Второй параметр - значение для крайней правой стороны экрана. Если наше окно - 640x480, значение по ширине будет 640. Поэтому крайняя правая сторона экрана равна 640. Поэтому наш экран по оси X изменяется от 0 до 640.



Третий параметр (высота) обычно был равен отрицательному числу, задающему нижний край экрана по оси Y. Но так как мы хотим использовать реальные значения пикселей, мы, не хотим иметь отрицательное число. Вместо этого мы сделаем низ экрана, равным высоте нашего окна. Если наше окно - 640x480, высота будет равна 480. Поэтому низ нашего экрана будет 480. Четвертый параметр обычно был равен положительному числу, задающему верхний край нашего экрана. Мы хотим, чтобы верхний край экрана был равным 0 (добрые старые координаты экрана), поэтому мы задаем четвертый параметр равным 0. При этом мы получим изменение от 0 до 480 по оси Y.



Последние два параметра - для оси Z. Мы не заботимся об оси Z, поэтому мы зададим диапазон от -1.0f до 1.0f. Нам будет достаточно того, что мы можем увидеть в 0.0f по оси Z.



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



GLvoid ReSizeGLScene(GLsizei width, GLsizei height) // Масштабирование и инициализация окна GL

{

  if (height==0)                 // Предотвращение деления на ноль

  {

    height=1;                    // Сделать высоту равной 1



  }



  glViewport(0,0,width,height);  // Сброс текущей области просмотра



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

  glLoadIdentity();              // Сброс матрицы проектирования



  glOrtho(0.0f,width,height,0.0f,-1.0f,1.0f); // Создание ортог. вида 640x480 (0,0 – верх лево)



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

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

}



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

После того, как шрифт создан, мы задаем настройки. Мы разрешаем плавное сглаживание, задаем черный цвет очистки экрана и значение 1.0f для очистки буфера глубины. После этого, следует новая строка кода.

Функция glHint() сообщает OpenGL о настройках вывода. В этом случае мы сообщаем OpenGL, что мы хотим, чтобы сглаживание линии было наилучшим (самым хорошим), насколько это возможно под OpenGL. Эта команда разрешает сглаживание (anti-aliasing).

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

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



int InitGL(GLvoid)            // Все настройки для OpenGL делаются здесь



{

  if (!LoadGLTextures())      // Переход на процедуру загрузки текстур

  {

    return FALSE;             // Если текстура не загружена, вернем ЛОЖЬ

  }



  BuildFont();                // Построение шрифта



  glShadeModel(GL_SMOOTH);    // Разрешить плавное сглаживание

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

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

  glHint(GL_LINE_SMOOTH_HINT, GL_NICEST); // Сглаживание линий

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

  glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); // Тип смешивания

  return TRUE;                // Инициализация окончена успешна

}



Теперь кода рисования. Это именно то место, где творится волшебство :).

Мы очищаем экран (черным) вместе с буфером глубины. Затем мы выбираем текстуру шрифта (texture[0]). Мы хотим вывести фиолетовым цветом слова "GRID CRAZY" (сумасшедшая сетка), поэтому мы задаем красный и синий полной интенсивности, а зеленый половиной интенсивности. После того, как мы выбрали цвет, мы вызываем glPrint(). Мы помещаем слова "GRID CRAZY" с 207 по оси X (в центре экрана) и с 24 по оси Y (вверху экрана). Мы используем наш увеличенный шрифт, при помощи выбора шрифта 0.

После того, как мы вывели "GRID CRAZY" на экран, мы изменяем цвет на желтый (полная интенсивность красного, полная интенсивность зеленого). Мы пишем на экране "Level:" (уровень) и значение переменной level2. Помните, что level2 может быть больше чем 3. level2 хранит значение уровня, которое игрок видит на экране. Выражение %2i означает, что мы не хотим больше чем двух цифр на экране для представления уровня. Спецификатор “i” означает, что значение - целое число.



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



int DrawGLScene(GLvoid)                 //Здесь мы будем рисовать

{

  glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); // Очистка экрана и буфера глубины

  glBindTexture(GL_TEXTURE_2D, texture[0]); // Выбор текстуры нашего шрифта

  glColor3f(1.0f,0.5f,1.0f);            // Установить фиолетовый цвет

  glPrint(207,24,0,"GRID CRAZY");       // Написать GRID CRAZY на экране

  glColor3f(1.0f,1.0f,0.0f);            // Установить желтый цвет

  glPrint(20,20,1,"Level:%2i",level2);  // Вывод состояние текущего уровня

  glPrint(20,40,1,"Stage:%2i",stage);   // Вывод состояние стадии



Теперь мы проверим, закончена ли игра. Если игра закончена, переменная gameover будет равна ИСТИНА. Если игра закончена, мы используем glColor3ub(r, г, b) чтобы выбрать случайный цвет. Отмечу, что мы использует 3ub вместо 3f. Используя 3ub, мы можем использовать целочисленные значения от 0 до 255 для задания цветов. Плюс в том, что при этом проще получить случайное значение от 0 до 255, чем получить случайное значение от 0.0f до 1.0f.

Как только случайный цвет был выбран, мы пишем слова "GAME OVER" (игра окончена) справа от заголовка игры. Справа под "GAME OVER" мы пишем "PRESS SPACE" (нажмите пробел). Это визуальное сообщение игроку, которое позволит ему узнать, что жизней больше нет и нажать пробел для перезапуска игры.



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

  {

    glColor3ub(rand()%255,rand()%255,rand()%255); // Выбор случайного цвета



    glPrint(472,20,1,"GAME OVER");   // Вывод GAME OVER на экран

    glPrint(456,40,1,"PRESS SPACE"); // Вывод PRESS SPACE на экран

  }



Если игрока еще имеются жизни, мы выводим анимированные изображения символа игрока справа от заголовка игры. Чтобы сделать это, мы создаем цикл, который начинается от 0 до текущего количества жизней игрока минус один. Я вычитаю один, потому что текущая жизнь это то изображение, которым Вы управляете.

Внутри цикла, мы сбрасываем область просмотра. После того, как область просмотра была сброшена, мы передвигаемся на 490 пикселей вправо плюс значение loop1 умноженное 40.0f. Это позволит, выводить каждую из анимированных жизней игрока сдвинутую друг относительно друга на 40 пикселей. Первое анимированное изображение будет выведено в 490 + (0*40) (= 490), второе анимированное изображение будет выведено в 490 + (1*40) (= 530), и т.д.

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

Затем выбираем зеленый цвет, и рисуем изображение. Способ вывода линий очень похож на рисование четырехугольника или многоугольника. Вы начинаете с glBegin(GL_LINES), сообщая OpenGL, что мы хотим вывести линию. Линии имеют 2 вершины. Мы используем glVertex2d, чтобы задать нашу первую точку. Функции glVertex2d не требуется указывать значение z. Эта функция хорошо нам подходит, так как мы не заботимся о значении z. Первая точка нарисована на 5 пикселей слева от текущего значения x и на 5 пикселей выше от текущего значения y. Это даст нам левую верхнюю точку. Вторая точка нашей первой линии рисуется на 5 пикселей справа от нашего текущего положения по x, и на 5 пикселей вниз. Это даст нам правую нижнюю точку. При этом будет нарисована линия от  левой верхней точки до правой нижней точки. Наша вторая линия будет нарисована от правой верхней точки до левой нижней точки. При этом будет нарисован зеленый символ "X" на экране.





После того, как мы вывели зеленый символ “X”, мы делаем вращение против часовой стрелки (по оси z) еще больше, но на этот раз с половиной скорости. Затем мы выбираем более темный оттенок зеленого (0.75f) и рисуем другой символ “X” размером 7 вместо 5. При этом будет выведен большой / темный символ “X” сверху первого зеленного символа “X”. Поскольку более темный символ “X” вращается медленнее, то возникнет иллюзия наличия сверху яркого символа “X” вращающихся усиков (смешок).



  for (loop1=0; loop1<lives-1; loop1++)          // Цикл по всем жизням минус текущая жизнь

  {

    glLoadIdentity();             // Сброс вида

    glTranslatef(490+(loop1*40.0f),40.0f,0.0f);  // Перенос вправо от нашего заголовка

    glRotatef(-player.spin,0.0f,0.0f,1.0f);      // Вращение против часовой стрелки

    glColor3f(0.0f,1.0f,0.0f);    // Цвет игрока зеленный

    glBegin(GL_LINES);            // Рисуем игрока с помощью линий

      glVertex2d(-5,-5);          // Лево верх игрока

      glVertex2d( 5, 5);          // Низ право

      glVertex2d( 5,-5);          // Верх право

      glVertex2d(-5, 5);          // Низ лево

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

    glRotatef(-player.spin*0.5f,0.0f,0.0f,1.0f); // Вращение против часовой стрелки

    glColor3f(0.0f,0.75f,0.0f);   // Установка темно-зеленного



    glBegin(GL_LINES);            // Рисуем игрока с помощью линий

      glVertex2d(-7, 0);          // Влево от центра игрока

      glVertex2d( 7, 0);          // Вправо от центра

      glVertex2d( 0,-7);          // Вверх от центра

      glVertex2d( 0, 7);          // Вниз от центра

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

  }



Теперь мы выводим сетку. Мы задаем значение переменной filled равной ИСТИНА. Это сообщит нашей программе, что сетка была полностью выведена (Вы увидите позже, зачем мы это делаем).

Затем мы устанавливаем ширину линии равной 2.0f. Это делает линии более толстыми, делая визуализацию сетки более четкой.

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

Вид сброшен, и мы начинаем два цикла. Переменная loop1 будет путешествовать слева направо. Переменная loop2 будет путешествовать сверху донизу.

Мы задаем синий цвет линии, затем мы проверяем, пройдена ли игроком эта горизонтальная линия, если это так, то мы задаем белый цвет. Значение hline[loop1][loop2] было бы равно ИСТИННА, если линия была пройдена, и ЛОЖЬ, если игрок не пробегал через нее.

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



Если переменная loop1 имеет правильное значение, мы проверяем, пройдена ли горизонтальная линия. Если это не так, то переменная filled установлена в ЛОЖЬ, сообщая, что есть, по крайней мере, одна линия, которая не была пройдена.

Затем линия рисуется. Мы выводим нашу первую горизонтальную линию (слева направо), начиная от 20+(0*60) (= 20). Эта линия выводится до 80 + (0*60) (= 80). Заметьте, что линия выведена слева направо. Именно поэтому мы не хотим вывести 11 (0-10) линий. Потому что последняя линия началась бы с правого края экрана и кончилась бы на 80 пикселей за экраном.



  filled=TRUE;                       // Задать True до начала тестирования

  glLineWidth(2.0f);                 // Задать ширину линий для ячеек 2.0f

  glDisable(GL_LINE_SMOOTH);         // Запретить сглаживание

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

  for (loop1=0; loop1<11; loop1++)   // Цикл слева направо

  {

    for (loop2=0; loop2<11; loop2++) // Цикл сверху вниз

    {

      glColor3f(0.0f,0.5f,1.0f);     // Задать синий цвет линии

      if (hline[loop1][loop2])       // Прошли горизонтальную линию?

      {

        glColor3f(1.0f,1.0f,1.0f);   // Если да, цвет линии белый

      }

      if (loop1<10)                  // Не рисовать на правом краю

      {



        if (!hline[loop1][loop2])    // Если горизонтальную линию не прошли

        {

          filled=FALSE;              // filled равно False

        }

        glBegin(GL_LINES);           // Начало рисования горизонтального бордюра ячейки

          glVertex2d(20+(loop1*60),70+(loop2*40)); // Левая сторона горизонтальной линии

          glVertex2d(80+(loop1*60),70+(loop2*40)); // Правая сторона горизонтальной линии

        glEnd();                     // Конец рисования горизонтального бордюра ячейки

      }



Код ниже делает то же самое, но при этом проверяется, что линия не выводится за нижний край экрана также как за правый край. Этот код ответствен за рисование вертикальных линий.



      glColor3f(0.0f,0.5f,1.0f);     // Задать синий цвет линии

      if (vline[loop1][loop2])       // Прошли вертикальную линию?

      {

        glColor3f(1.0f,1.0f,1.0f);   // Если да, цвет линии белый

      }

      if (loop2<10)                  // Не рисовать на нижнем краю

      {

        if (!vline[loop1][loop2])    // Если вертикальную линию не прошли



        {

          filled=FALSE;              // filled равно False

        }

        glBegin(GL_LINES);           // Начало рисования вертикального бордюра ячейки

          glVertex2d(20+(loop1*60),70+(loop2*40));  // Верхняя сторона вертикальной линии

          glVertex2d(20+(loop1*60),110+(loop2*40)); // Нижняя сторона вертикальной линии

        glEnd();                     // Конец рисования вертикального бордюра ячейки

      }



Теперь мы проверим, пройдены ли все 4 стороны ячейки. Каждая ячейка на экране занимает 1/100-ая часть картинки полноэкранного экрана. Поскольку каждая ячейка часть большой текстуры, мы должны вначале разрешить отображение текстуры. Мы не хотим, чтобы текстура была подкрашена в красный, зеленый или синий, поэтому мы устанавливаем ярко белый цвет. После того, как цвет задан, мы выбираем нашу текстуру сетки (texture[1]).

Затем мы проверяем наличие ячейки на экране. Вспомните, что наш цикл рисует 11 линий справа и налево, и 11 линий сверху и вниз. Но мы не имеем 11 ячеек по одной линии. Мы имеем 10 ячеек. Поэтому мы не должны проверять 11-ую позицию. Для этого надо проверить, что и loop1 и loop2, меньше чем 10. Это даст 10 ячеек от 0 - 9.

После того, как мы будем уверены, что мы не выходим за диапазон, мы можем начинать проверять границы. hline[loop1][loop2] - верх ячейки. hline[loop1][loop2+1] - низ ячейки. vline[loop1][loop2] - левая сторона ячейки, и vline[loop1+1][loop2] - правая сторона ячейки. Я надеюсь, что следующий рисунок вам поможет:









Все горизонтальные линии получаются от loop1 до loop1+1. Как Вы можете видеть, первая горизонтальная линия создается при loop2. Вторая горизонтальная линия создается при loop2+1. Вертикальные линии получаются от loop2 до loop2+1. Первая вертикальная линия создается при loop1, и вторая вертикальная линия создается при loop1+1.

Когда переменная loop1 увеличивается, правая сторона нашей старой ячейки становится левой стороной новой ячейки. Когда переменная loop2 увеличивается, низ старой ячейки становится вершиной новой ячейки.

Если все 4 бордюра ИСТИННЫ (это означает, что мы прошли через все) мы можем наложить текстуру на блок. Мы сделаем это тем же самым способом, с помощью которого мы разделили текстуру шрифта на отдельные символы. Мы делим, и loop1 и loop2 на 10, потому что мы хотим наложить текстуру на 10 ячеек слева направо, и на 10 ячеек сверху и вниз. Координаты текстуры меняются от 0.0f до 1.0f, и 1/10-ый от 1.0f будет 0.1f.

Поэтому для вычисления координат правого верхнего угла нашего блока мы делим значения цикла на 10 и добавляем 0.1f к x координате текстуры. Чтобы получить координаты левого верхнего угла блока, мы делим наше значение цикла на 10. Чтобы получить координаты левого нижнего угла блока, мы делим наше значение цикла на 10 и добавляем 0.1f к y координате текстуры. Наконец, чтобы получить координаты правого нижнего угла текстуры, мы делим значение цикла на 10 и добавляем 0.1f, и к x и к y координатам текстуры.

Небольшой пример: loop1=0 и loop2=0