Урок 21. Линии, сглаживание, синхронизация, ортографическая проекция и звуки.
Добро пожаловать на 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
· Правая X координата текстуры = loop1/10+0.1f = 0/10+0.1f = 0+0.1f = 0.1f
· Левая X координата текстуры = loop1/10 = 0/10 = 0.0f
· Верх Y координата текстуры = loop2/10 = 0/10 = 0.0f;
· Низ Y координата текстуры = loop2/10+0.1f = 0/10+0.1f = 0+0.1f
= 0.1f;
loop1=1 и loop2=1
· Правая X координата текстуры = loop1/10+0.1f = 1/10+0.1f = 0.1f+0.1f = 0.2f
· Левая X координата текстуры = loop1/10 = 1/10 = 0.1f
· Верх Y координата текстуры = loop2/10 = 1/10 = 0.1f;
· Низ Y координата текстуры = loop2/10+0.1f = 1/10+0.1f = 0.1f+0.1f
= 0.2f;
Буду надеяться, что это все имеет смысл. Если бы loop1 и loop2 были бы равны 9, мы закончили бы со значениями 0.9f и 1.0f. Поэтому, как вы можете видеть, наши координаты текстуры наложенной на 10 блоков меняются от наименьшего значения 0.0f до наибольшего значения 1.0f. Т.е. наложение всей текстуры на экран. После того, как мы наложили часть текстуры на экран, мы отключаем наложение текстуры. После того, как мы нарисовали все линии и заполнили все блоки, мы задаем ширину линий равной 1.0f.
glEnable(GL_TEXTURE_2D); // Разрешение наложение текстуры
glColor3f(1.0f,1.0f,1.0f); // Ярко белый свет
glBindTexture(GL_TEXTURE_2D, texture[1]); // Выбор мозаичного изображения
if ((loop1<10) && (loop2<10)) // Если в диапазоне, заполнить пройденные ячейки
{
// Все ли стороны ячейки пройдены?
if (hline[loop1][loop2] && hline[loop1][loop2+1] &&
vline[loop1][loop2] && vline[loop1+1][loop2])
{
glBegin(GL_QUADS); // Нарисовать текстурированный четырехугольник
glTexCoord2f(float(loop1/10.0f)+0.1f,1.0f-(float(loop2/10.0f)));
glVertex2d(20+(loop1*60)+59,(70+loop2*40+1)); // Право верх
glTexCoord2f(float(loop1/10.0f),1.0f-(float(loop2/10.0f)));
glVertex2d(20+(loop1*60)+1,(70+loop2*40+1)); // Лево верх
glTexCoord2f(float(loop1/10.0f),1.0f-(float(loop2/10.0f)+0.1f));
glVertex2d(20+(loop1*60)+1,(70+loop2*40)+39); // Лево низ
glTexCoord2f(float(loop1/10.0f)+0.1f,1.0f-(float(loop2/10.0f)+0.1f));
glVertex2d(20+(loop1*60)+59,(70+loop2*40)+39); // Право низ
glEnd(); // Закончить текстурирование ячейки
}
}
glDisable(GL_TEXTURE_2D); // Запрет наложения текстуры
}
}
glLineWidth(1.0f); // Ширина линий 1.0f
Код ниже проверяет, равно ли значение переменной anti ИСТИНА. Если это так, то мы разрешаем сглаживание линий.
if (anti) // Anti TRUE?
{
glEnable(GL_LINE_SMOOTH); // Если так, то разрешить сглаживание
}
Чтобы сделать игру немного проще я добавил специальный предмет.
Этим предметом будут песочные часы. Когда Вы касаетесь песочных часов, противники
замерзают на определенное количество времени. Следующий раздел кода ответственен
за вывод песочных часов.
Для песочных часов мы используем x и y, чтобы
позиционировать таймер, но в отличие от нашего игрока и противников, мы не используем
fx и fy для точного позиционирования. Вместо этого мы будем использовать
fx, чтобы следить, действительно
ли часы отображаются. fx
будет равно 0, если часы не видимы, и 1, если они видимы, и 2, если игрок коснулся
часов. fy будет использоваться как счетчик, для
отслеживания как давно видны или не видны часы.
Поэтому вначале мы проверяем, видны ли часы. Если нет, мы обходим
код вывода часов. Если часы видны, мы сбрасываем матрицу вида модели, и позиционируем
часы. Поскольку наша первая точка сетки находится на 20 пикселей слева, мы добавим
20 к hourglass.x умножим на 60. Мы умножаем hourglass.x
на 60, потому что точки на нашей сетке слева направо отстоят друг от друга на
60 пикселей. Затем мы позиционируем песочные часы по оси Y. Мы добавляем 70
к hourglass.y
умножаем на 40, потому что мы хотим начать рисовать на 70 пикселей вниз от верхнего
края экрана. Каждая точка на нашей сетке сверху внизу отстоит друг от друга
на 40 пикселей.
После того, как мы завершили позиционирование песочных часов, мы можем вращать их по оси Z. hourglass.spin используется, чтобы следить за вращением, так же как player.spin следит за вращением игрока. Прежде, чем мы начинаем выводить песочные часы, мы выбираем случайный цвет.
if (hourglass.fx==1) // Если fx=1 нарисовать песочные часы
{
glLoadIdentity(); // Сброс матрицы вида модели
glTranslatef(20.0f+(hourglass.x*60),70.0f+(hourglass.y*40),0.0f); // Поместим часы
glRotatef(hourglass.spin,0.0f,0.0f,1.0f); // Вращаем по часовой стрелке
glColor3ub(rand()%255,rand()%255,rand()%255); // Зададим случайный цвет часов
Вызов функции glBegin(GL_LINES) сообщает OpenGL, что мы хотим нарисовать линии. Вначале мы смещаемся на 5 пикселей влево и вверх от нашего текущего положения. При этом мы получим левую верхнюю вершину наших песочных часов. OpenGL начнет рисовать линию от этого положения. Конец линии будет вправо и вниз на 5 пикселей от нашего первоначального положения. При этом наша линия, пройдет от левой верхней точки до правой нижней точки. Сразу же после этого мы выводим вторую линию, проходящую от правой верхней точки до левой нижней точки. Это даст нам символ 'X'. В конце мы соединяем две нижние точки вместе, и затем две верхние точки, чтобы создать объект типа песочных часов :).
glBegin(GL_LINES); // Начало рисования наших песочных часов линиями
glVertex2d(-5,-5); // Лево Верх песочных часов
glVertex2d( 5, 5); // Право Низ песочных часов
glVertex2d( 5,-5); // Право Верх песочных часов
glVertex2d(-5, 5); // Лево Низ песочных часов
glVertex2d(-5, 5); // Лево Низ песочных часов
glVertex2d( 5, 5); // Право Низ песочных часов
glVertex2d(-5,-5); // Лево Верх песочных часов
glVertex2d( 5,-5); // Право Верх песочных часов
glEnd(); // Конец рисования песочных часов
}
Теперь мы рисуем нашего игрока. Мы сбрасываем матрицу вида модели, и позиционируем игрока на экране. Заметьте, что мы позиционируем игрока, используя fx и fy. Мы хотим, чтобы игрок двигался плавно, поэтому мы используем точное позиционирование. После позиционирования игрока, мы вращаем игрока относительно оси Z, используя player.spin. Мы задаем светло зеленный цвет и начинаем рисовать. Примерно так же как мы вывели песочные часы, мы выводим символ 'X'. Начинаем с левой верхней точки до правой нижней точки, затем с правой верхней точки до левой нижней точки.
glLoadIdentity(); // Сброс матрицы вида модели
glTranslatef(player.fx+20.0f,player.fy+70.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(); // Конец рисования игрока
Рисование не слишком разнообразных объектов может разочаровать.
Я не хотел бы, чтобы игрок выглядел скучновато, поэтому я добавил следующий
раздел кода, для того чтобы создать большое и быстро вращающиеся лезвие поверх
игрока, которого мы только что нарисовали выше. Мы вращаем относительно оси
Z лезвие на player.spin умножив его на 0.5f. Поскольку мы вращаем еще раз, будет казаться,
что эта часть игрока перемещается немного быстрее, чем первая часть игрока.
После выполнения нового вращения, мы меняем цвет на более темный оттенок зеленного. Так, чтобы казалось, что игрок, сделан из различных цветов / частей. Затем мы выводим большой '+' сверху первой части игрока. Он будет больше, потому что мы используем -7 и +7 вместо -5 и +5. Также заметьте, что вместо рисования от одного угла до другого, я рисую эту часть игрока слева направо и сверху вниз.
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(); // Конец рисования игрока
Теперь нам осталось вывести противников, и мы закончим рисование
:). Вначале мы организуем цикл по числу всех противников, которые есть на текущем
уровне. Мы вычисляем, сколько противников надо рисовать, умножив нашу текущую
игровую стадию на внутренний игровой уровень. Вспомните, что каждый уровень
имеет 3 стадии, и максимальное значение внутреннего уровня равно 3. Поэтому
мы можем иметь максимум 9 противников.
Внутри цикла мы сбрасываем матрицу просмотра вида, и позиционируем
текущего противника (enemy[loop1]). Мы позиционируем
противника, используя его точные значения x и y (fx и fy). После позиционирования текущего противника мы задаем розовый
цвет и начинаем рисование.
Первая линия пройдет от 0,-7 (7 пикселей верх от начального положения) к -7,0 (7 пикселей влево от начального положения). Вторая линия пройдет от -7,0 до 0,7 (7 пикселей вниз от начального положения). Третья линия пройдет от 0,7 до 7,0 (7 пикселей вправо от нашего начального положения), и последняя линия пройдет от 7,0 назад к началу первой линии (7 пикселей верх от начального положения). При этом на экране получится не вращающийся розовый алмаз.
for (loop1=0; loop1<(stage*level); loop1++) // Цикл рисования противников
{
glLoadIdentity(); // Сброс матрицы просмотра вида
glTranslatef(enemy[loop1].fx+20.0f,enemy[loop1].fy+70.0f,0.0f);
glColor3f(1.0f,0.5f,0.5f); // Сделать тело противника розовым
glBegin(GL_LINES); // Начало рисования противника
glVertex2d( 0,-7); // Верхняя точка тела
glVertex2d(-7, 0); // Левая точка тела
glVertex2d(-7, 0); // Левая точка тела
glVertex2d( 0, 7); // Нижняя точка тела
glVertex2d( 0, 7); // Нижняя точка тела
glVertex2d( 7, 0); // Правая точка тела
glVertex2d( 7, 0); // Правая точка тела
glVertex2d( 0,-7); // Верхняя точка тела
glEnd(); // Конец рисования противника
Мы не хотим, чтобы враги выглядели невзрачно, поэтому мы добавим темно красное вращающиеся лезвие ('X') сверху алмаза, который мы только что нарисовали. Мы вращаем его относительно оси Z на enemy[loop1].spin, и затем выводим 'X'. Мы начинаем с левого верхнего угла и рисуем линию к правому нижнему углу. Затем мы рисуем вторую линию с правого нижнего угла до левого нижнего угла. Эти две линии пересекают друг с другом, и при этом получается символ 'X' (или клинок… смешок).
glRotatef(enemy[loop1].spin,0.0f,0.0f,1.0f); // Вращение клинка противника
glColor3f(1.0f,0.0f,0.0f); // Сделаем клинок противника красным
glBegin(GL_LINES); // Начало рисования клинка противника
glVertex2d(-7,-7); // Лево верх противника
glVertex2d( 7, 7); // Право низ противника
glVertex2d(-7, 7); // Лево низ противника
glVertex2d( 7,-7); // Право верх противника
glEnd(); // Конец рисования противника
}
return TRUE; // Все OK
}
Я добавил вызов функции KillFont() в конце KillGLWindow(). При этом мы будем уверены, что список отображения шрифта удален, когда окно будет уничтожено.
GLvoid KillGLWindow(GLvoid) // Корректное удаление окна
{
if (fullscreen) // Мы в полноэкранном режиме?
{
ChangeDisplaySettings(NULL,0); // Если это так, то переключиться на рабочий стол
ShowCursor(TRUE); // Показать курсор мыши
}
if (hRC) // У нас есть контекст визуализации?
{
if (!wglMakeCurrent(NULL,NULL)) // Мы можем освободить контексты DC и RC?
{
MessageBox(NULL,"Release Of DC And RC Failed.","SHUTDOWN ERROR",
MB_OK | MB_ICONINFORMATION);
}
if (!wglDeleteContext(hRC)) // Мы можем удалить RC?
{
MessageBox(NULL,"Release Rendering Context Failed.","SHUTDOWN ERROR",
MB_OK | MB_ICONINFORMATION);
}
hRC=NULL; // Задать RC в NULL
}
if (hDC && !ReleaseDC(hWnd,hDC)) // Мы можем освободить DC?
{
MessageBox(NULL,"Release Device Context Failed.","SHUTDOWN ERROR",
MB_OK | MB_ICONINFORMATION);
hDC=NULL; // Задать DC в NULL
}
if (hWnd && !DestroyWindow(hWnd)) // Мы можем уничтожить окно?
{
MessageBox(NULL,"Could Not Release hWnd.","SHUTDOWN ERROR",
MB_OK | MB_ICONINFORMATION);
hWnd=NULL; // Задать hWnd в NULL
}
if (!UnregisterClass("OpenGL",hInstance)) // Мы можем удалить регистрацию класса?
{
MessageBox(NULL,"Could Not Unregister Class.","SHUTDOWN ERROR",
MB_OK | MB_ICONINFORMATION);
hInstance=NULL; // Задать hInstance в NULL
}
KillFont(); // Уничтожить фонт, который мы сделали
}
Код CreateGLWindow() и WndProc() не изменил, поэтому идите вниз пока не встретите следующий раздел кода.
int WINAPI WinMain(
HINSTANCE hInstance, // Экземпляр
HINSTANCE hPrevInstance, // Предыдущий экземпляр
LPSTR lpCmdLine, // Параметры командной строки
int nCmdShow) // Показать состояние окна
{
MSG msg; // Структура сообщения окна
BOOL done=FALSE; // Булевская переменная выхода из цикла
// Запросим пользователя какой режим отображения он предпочитает
if (MessageBox(NULL,"Would You Like To Run In Fullscreen Mode?",
"Start FullScreen?",MB_YESNO|MB_ICONQUESTION)==IDNO)
{
fullscreen=FALSE; // Оконный режим
}
Этот раздел кода не много изменился. Я изменил заголовок окна на " Урок по линиям NeHe", и я добавил вызов функции ResetObjects(). При этом игрок позиционируется в левой верхней точке сетки, и противникам задаются случайные начальные положения. Враги будут всегда стартовать, по крайней мере, на 5 ячеек от Вас. Функция TimerInit() корректно инициализирует таймер.
// Создадим наше окно OpenGL
if (!CreateGLWindow("NeHe's Line Tutorial",640,480,16,fullscreen))
{
return 0; // Выходим если окно не было создано
}
ResetObjects(); // Установка стартовых позиций Игрока / Противников
TimerInit(); // Инициализация таймера
while (!done) // Цикл, который продолжается пока done=FALSE
{
if (PeekMessage(&msg,NULL,0,0,PM_REMOVE)) // Есть ожидаемое сообщение?
{
if (msg.message==WM_QUIT) // Мы получили сообщение о выходе?
{
done=TRUE; // Если так done=TRUE
}
else // Если нет, продолжаем работать с сообщениями окна
{
TranslateMessage(&msg); // Переводим сообщение
DispatchMessage(&msg); // Отсылаем сообщение
}
}
else // Если сообщений нет
{
Теперь cделаем работу по синхронизации.
Вначале запомните, что перед выводом нашей сцены, мы запоминаем время в переменной
с плавающей запятой, которая названа start. Затем
мы выводим сцену и переключаем буфера.
Сразу же после того, как мы переключили буфера, мы делаем задержку.
Мы делаем при помощи сравнения текущего значения таймера (TimerGetTime()) с нашим стартовым значением плюс шаг скорости игры
умноженный на 2. Если текущее значение таймера меньше чем значение, которое
мы хотим, мы повторяем цикл, пока текущее значение таймера не будет равно или
большее чем значение, которое мы хотим. Это ДЕЙСТВИТЕЛЬНО замедляет быстрые
системы.
Поскольку мы используем шаги скорости (с помощью adjust)
программа будет всегда выполняться с той же самой скоростью. Например, если
бы наш шаг скорости был бы 1, мы ждали бы, пока таймер не был равен или больше
чем 2 (1*2). Но если мы увеличим шаг скорости до 2 (что вызовет перемещение
игрока на удвоенное число пикселей одновременно), задержка увеличиться на 4
(2*2). Поэтому даже при том, что мы перемещаем в два раза быстрее, задержка
также удвоится, поэтому игра будет выполняться с той же самой скоростью :).
Есть один прием, который многие делают — берут текущее время, и вычитают из него старое время, чтобы выяснить, сколько времени прошло. Затем они перемещают объекты на некоторое расстояние, основанное на значении времени, которое прошло. К сожалению, я не могу этого сделать в этой программе, потому что точное перемещение должно быть таким, чтобы игрок мог попасть на линии сетки. Если текущая точная позиция x была 59, и компьютер решил переместить игрока, на два пикселя, игрок никогда не попадет на вертикальную линию в позиции 60 на сетке.
float start=TimerGetTime(); // Захват времени до начала рисования
// Нарисовать сцену. Отследить нажатие на клавишу ESC и
// приход сообщения о выходе из DrawGLScene()
if ((active && !DrawGLScene()) || keys[VK_ESCAPE]) // Активно? Выход принят?
{
done=TRUE; // ESC или DrawGLScene сигнализирует о выходе
}
else // Не время выходить, надо обновить сцену
{
SwapBuffers(hDC); // Переключить буфера (Двойная Буферизация)
}
// Отбросим циклы на быстрой системе
while(TimerGetTime()<start+float(steps[adjust]*2.0f)) {}
Следующий код мало изменился. Я изменил заголовок окна на "Урок NeHe по линиям".
if (keys[VK_F1]) // Была нажата кнопка F1?
{
keys[VK_F1]=FALSE; // Если так - установим значение FALSE
KillGLWindow(); // Закроем текущее окно OpenGL
fullscreen=!fullscreen; // Переключим режим "Полный экран"/"Оконный"
// Заново создадим наше окно OpenGL
if (!CreateGLWindow("NeHe's Line Tutorial",640,480,16,fullscreen))
{
return 0; // Выйти, если окно не было создано
}
}
В этой секции кода проверяется, нажата ли клавиша и не удерживается
ли она. Если 'A' нажата, ap
станет ИСТИНА (сообщая нашей программе, что ‘A’ опущена
вниз), и anti переключается из ИСТИНЫ в ЛОЖЬ
или из ЛОЖИ в ИСТИНУ. Помните, что значение anti
проверяется в коде рисования, чтобы узнать включено ли сглаживание или нет.
Если клавиша 'A' была отпущена (ЛОЖЬ) тогда значение ap будет ЛОЖЬ сообщая программе, что эта клавиша больше не удерживается.
if (keys['A'] && !ap) // Если клавиша 'A' нажата и не удерживается
{
ap=TRUE; // ap равно TRUE
anti=!anti; // Переключим сглаживание
}
if (!keys['A']) // Если клавиша 'A' отпущена
{
ap=FALSE; // ap равно FALSE
}
Теперь как перемещать противников. Я стремился сделать этот
раздел кода как можно проще. Здесь очень немного логики. В основном, враги следят
за тем, где Вы находитесь, и они двигаются в этом направлении. Поскольку я проверяю
фактические x и y позиции игроков и не проверяю точные значения,
игрокам может казаться, что враги имеют некоторый интеллект. Враги могут видеть,
что Вы наверху экрана. Но к тому времени, когда точные значения совпадут с верхом
экрана, Вы можете уже быть в другом месте. Это заставляет их иногда двигаться
мимо Вас, прежде чем они поймут, что Вы больше не там, где они думают. Они как
будто глухи, но поскольку они иногда двигаются мимо Вас, Вы может оказаться
в окружении.
Мы начнем с проверки того, что игра еще не закончена, и что
окно (если в оконном режиме) является все еще активным. При помощи этой проверки
active делаем так, чтобы враги не двигались,
когда окно свернуто. Это даст Вам удобную паузу, когда Вы захотите перерваться
:).
После того, как мы проверили то, что враги должны быть перемещены, мы запускаем цикл. В этом цикле мы проходим по всем видимым противникам. Снова мы вычисляем, сколько противников должно быть на экране, умножая текущую стадию на текущий внутренний уровень.
if (!gameover && active) // Если игра не окончена и программа активна — передвинуть объекты
{
for (loop1=0; loop1<(stage*level); loop1++) // Цикл по противникам
{
Теперь мы перемещаем текущего противника (enemy[loop1]). Вначале мы проверяем меньше ли x
позиция противника, чем x позиция игрока, и мы контролируем, что точная
позиция y противника выровнена с горизонтальной линией. Мы не можем перемещать
противника влево или вправо, если он не на горизонтальной линии. Если бы мы
сделали, враг прошел бы через середину ячейки, сделав игру очень сложной :).
Если x позиция противника меньше, чем x позиция
игрока, и точная позиция y противника выровнена с горизонтальной линией,
мы передвигаем противника по x на одну клетку ближе к текущей позиции
игрока.
Подобным образом мы делаем это, чтобы переместить противника,
влево, вниз и вверх. При перемещении вверх или вниз, мы должны проконтролировать,
что точная позиция x противника выровнена с вертикальной линией. Мы не
хотим, чтобы враг срезал через верх или низ ячейки.
Примечание: изменение x и y позиций противников не перемещает противника на экране. Вспомните, что, когда мы рисовали противников, мы использовали точные позиции, чтобы разместить противников на экране. Изменение x и y позиций только сообщает нашей программе, где мы ХОТИМ, чтобы противники двигались.
if ((enemy[loop1].x<player.x) && (enemy[loop1].fy==enemy[loop1].y*40))
{
enemy[loop1].x++; // Сдвиг противника вправо
}
if ((enemy[loop1].x>player.x) && (enemy[loop1].fy==enemy[loop1].y*40))
{
enemy[loop1].x--; // Сдвиг противника влево
}
if ((enemy[loop1].y<player.y) && (enemy[loop1].fx==enemy[loop1].x*60))
{
enemy[loop1].y++; // Сдвиг противника вниз
}
if ((enemy[loop1].y>player.y) && (enemy[loop1].fx==enemy[loop1].x*60))
{
enemy[loop1].y--; // Сдвиг противника вверх
}
В этом коде фактически реализовано перемещение. Мы проверяем,
больше ли значение переменной delay, чем 3 минус текущий внутренний уровень. Т.е., если
наш текущий уровень равен 1 программа, сделает цикл 2 раза (3-1) прежде, чем
враги фактически сдвинутся. На уровне 3 (самый высокий уровень) враги будут
перемещаться с той же самой скоростью как игрок (без задержек). Мы также контролируем,
что hourglass.fx не равен 2. Вспомните, если hourglass.fx равно 2, то это означает, что игрок коснулся песочных часов. Враги
при этом не должны перемещаться.
Если delay больше, чем с 3-level, и игрок не коснулся песочных часов, мы перемещаем противников, изменения точные позиции противников (fx и fy). Вначале мы присваиваем delay снова 0 так, чтобы мы могли запустить счетчик delay снова. Затем мы запускаем цикл, который проходит по всем видимым противникам (stage * level).
// Если наша задержка истекла, и игрок не коснулся песочных часов
if (delay>(3-level) && (hourglass.fx!=2))
{
delay=0; // Сброс задержки
for (loop2=0; loop2<(stage*level); loop2++) // Цикл по всем противникам
{
Для перемещения противников, мы проверяем, нужно ли текущего
противника (enemy[loop2]) двигать в заданном
направлении, чтобы установить противника в x и y позицию, которую
мы хотим. В первой строке ниже мы проверяем, является ли точная позиция противника
по оси X меньше, чем нужная позиции x умноженная на 60. Вспомните,
что размер каждой клетки равен 60 пикселям по горизонтали. Если точная позиция
x меньше, чем x позиция противника умноженная на 60, мы сдвигаем
противника направо на steps[adjust]
(скорость нашей игры зависит от значения adjust). Мы также вращаем противника по часовой стрелке,
чтобы казалось, что он катится направо. Для этого мы увеличиваем enemy[loop2].spin
на steps[adjust] (текущая скорость
игры, которая зависит от adjust).
Затем мы проверяем, является ли значение fx
противника больше, чем позиция x противника умноженная на 60 и если это
так, мы перемещаем противника влево и вращаем противника влево.
То же самое мы делаем при перемещении противника вверх и вниз. Если позиция y противника меньше, чем позиция fy противника умноженная на 40 (40 пикселей размер ячейки по вертикали) мы увеличиваем fy, и вращаем противника, чтобы казалось, что он катится вниз. Наконец, если позиция y больше, чем позиция fy умноженная на 40, мы уменьшаем значение fy, чтобы переместить противника вверх. Снова, вращаем противника, чтобы казалось, что он катится вверх.
// Точная позиция по оси X меньше чем назначенная позиция?
if (enemy[loop2].fx<enemy[loop2].x*60)
{
enemy[loop2].fx+=steps[adjust]; // Увеличим точную позицию по оси X
enemy[loop2].spin+=steps[adjust]; // Вращаем по часовой
}
// Точная позиция по оси X больше чем назначенная позиция?
if (enemy[loop2].fx>enemy[loop2].x*60)
{
enemy[loop2].fx-=steps[adjust]; // Уменьшим точную позицию по оси X
enemy[loop2].spin-=steps[adjust]; // Вращаем против часовой
}
// Точная позиция по оси Y меньше чем назначенная позиция?
if (enemy[loop2].fy<enemy[loop2].y*40)
{
enemy[loop2].fy+=steps[adjust]; // Увеличим точную позицию по оси Y
enemy[loop2].spin+=steps[adjust]; // Вращаем по часовой
}
// Точная позиция по оси Y больше чем назначенная позиция?
if (enemy[loop2].fy>enemy[loop2].y*40)
{
enemy[loop2].fy-=steps[adjust]; // Уменьшим точную позицию по оси Y
enemy[loop2].spin-=steps[adjust]; // Вращаем против часовой
}
}
}
После перемещения противников мы проверяем, попал ли кто-нибудь
из них в игрока. Для точности мы сравниваем точные позиции противников с точной
позицией игрока. Если позиция противника fx равна точной позиция fx игрока, и позиция fy противника равна fy
игрока, то игрок МЕРТВ :).
Если игрок мертв, то мы уменьшаем его количество жизней. Затем
мы проверяем, что у игрока еще есть жизни. Это можно сделать сравнением lives
с 0. Если lives равно нулю, то мы присваиваем gameover ИСТИНА.
Затем мы сбрасываем наши объекты, вызывая ResetObjects(), и проигрываем звук смерти.
Вывод звука новый материал в этом уроке. Я решил использовать
наиболее простую процедуру вывода звука … PlaySound().
PlaySound() имеет три параметра. В первом параметре мы передаем
ей название файла, который мы хотим проиграть. В нашем случае мы хотим, чтобы
проиграл звук из файла Die.WAV
в каталоге Data. Второй параметр можно проигнорировать. Мы установим его в NULL.
Третий параметр — флаг для проигрывания звука. Два наиболее часто используемых
типа флага: SND_SYNC, который приостанавливает выполнение программы пока
звук не проиграет, и SND_ASYNC, который запускает проигрывание звука,
но не останавливает программу. Мы хотим иметь небольшую задержку после того,
как игрок умер, поэтому мы используем SND_SYNC. Довольно просто!
Я забыл рассказать об одной вещи в начале программы: для того чтобы PlaySound() и таймер работали, Вы должны подключить файл winmm.lib в проект (в Visual C++ это делается в PROJECT / SETTINGS / LINK). winmm.lib — мультимедийная библиотека Windows. Если Вы не включите эту библиотеку, Вы получите сообщения об ошибках, когда Вы пробуете откомпилировать программу.
// Кто-нибудь из противников сверху игрока?
if ((enemy[loop1].fx==player.fx) && (enemy[loop1].fy==player.fy))
{
lives--; // Уменьшим жизни
if (lives==0) // Нет больше жизней?
{
gameover=TRUE; // gameover равно TRUE
}
ResetObjects(); // Сброс позиций игрока / противников
PlaySound("Data/Die.wav", NULL, SND_SYNC); // Играем звук смерти
}
}
Теперь мы можем переместить игрока. В первой строке кода ниже
мы проверяем, нажата ли стрелка вправо, и player.x меньше, чем 10 (не хотим выйти из сетки), и player.fx
равно player.x
умноженное на 60, и player.fy равно player.y умноженное на 40, т.е. находится в месте пересечения X
и Y линий сетки.
Если мы не проверим, что игрок был в месте пересечения, и разрешим
игроку перемещать как угодно, то игрок
срежет правый угол ячейки, точно так же как противники сделали бы, если бы мы
не проверяли, что они выровнены с вертикальной или горизонтальной линией. Эта
проверка также делается, для того чтобы проверить, что игрок закончил, передвигаться
прежде, чем мы переместим его в новое местоположение.
Если игрок в месте пересечения сетки (где встречаются вертикальные
и горизонтальные линии) и он не за правым краем, мы помечаем, что текущая горизонтальная
линия пройдена. Затем мы увеличиваем значение player.x на единицу, что вызывает перемещение
игрока на одну клетку вправо.
Далее мы делаем также при перемещении влево, вниз и вверх. Когда
перемещаем влево, мы проверяем, что игрок не вышел за левый край сетки. Когда
перемещаем вниз, мы проверяем, что игрок не покинул сетку снизу, и при перемещении
вверх мы проверяем, что игрок не вылетел за верх сетки.
При перемещении влево и вправо мы помечаем горизонтальную линию (hline[][]) ИСТИНА, что означает, что она пройдена. При перемещении вверх и вниз мы помечаем вертикальную линию (vline[][]) ИСТИНА, что означает, что она пройдена.
if (keys[VK_RIGHT] && (player.x<10) && (player.fx==player.x*60) && (player.fy==player.y*40))
{
// Пометить текущую горизонтальную границу как пройденную
hline[player.x][player.y]=TRUE;
player.x++; // Переместить игрока вправо
}
if (keys[VK_LEFT] && (player.x>0) && (player.fx==player.x*60) && (player.fy==player.y*40))
{
player.x--; // Переместить игрока влево
// Пометить текущую горизонтальную границу как пройденную
hline[player.x][player.y]=TRUE;
}
if (keys[VK_DOWN] && (player.y<10) && (player.fx==player.x*60) && (player.fy==player.y*40))
{
// Пометить текущую вертикальную границу как пройденную
vline[player.x][player.y]=TRUE;
player.y++; // Переместить игрока вниз
}
if (keys[VK_UP] && (player.y>0) && (player.fx==player.x*60) && (player.fy==player.y*40))
{
// Пометить текущую вертикальную границу как пройденную
player.y--; // Переместить игрока вверх
vline[player.x][player.y]=TRUE;
}
Мы увеличиваем / уменьшаем точные fx
и fy переменные игрока, так же как мы увеличиваем / уменьшаем
точные fx и fy переменные противника.
Если значение fx игрока, меньше чем значение
x игрока умноженное на 60, мы увеличиваем fx
игрока, на шаг скорости нашей игры в зависимости от значения adjust.
Если значение fx игрока больше, чем x
игрока умноженное на 60, мы уменьшаем fx игрока, на шаг скорости нашей игры в
зависимости от значения adjust.
Если значение fy игрока, меньше чем y
игрока умноженное на 40, мы увеличиваем fy игрока, на шаг скорости нашей
игры в зависимости от значения adjust.
Если значение fy игрока, больше чем y
игрока умноженное на 40, мы уменьшаем fy игрока, на шаг скорости нашей
игры в зависимости от значения adjust.
if (player.fx<player.x*60) // Точная позиция по оси X меньше чем назначенная позиция?
{
player.fx+=steps[adjust]; // Увеличим точную позицию X
}
if (player.fx>player.x*60) // Точная позиция по оси X больше чем назначенная позиция?
{
player.fx-=steps[adjust]; // Уменьшим точную позицию X
}
if (player.fy<player.y*40) // Точная позиция по оси Y меньше чем назначенная позиция?
{
player.fy+=steps[adjust]; // Увеличим точную позицию Y
}
if (player.fy>player.y*40) // Точная позиция по оси Y больше чем назначенная позиция?
{
player.fy-=steps[adjust]; // Уменьшим точную позицию X
}
}
Если игра завершена, то будет запущен следующий небольшой раздел
кода. Мы проверяем, нажатие клавиши пробел. Если это так, то мы присваиваем
переменной gameover ЛОЖЬ (повторный запуск игры). Мы
задаем значение переменной filled ИСТИНА. Это
означает, что стадия окончена, вызывая сброс переменных игрока, вместе с противниками.
Мы задаем стартовый уровень равным 1, наряду с реальным отображенным уровнем (level2). Мы устанавливаем значение переменной stage равной 0. Мы делаем это, потому что после того, как компьютер видит, что сетка была заполнена, он будет думать, что Вы закончили стадию, и увеличит stage на 1. Поскольку мы устанавливаем stage в 0, то затем stage увеличивается, и станет равной 1 (точно, что мы хотим). Наконец мы устанавливаем lives обратно в 5.
else // Иначе
{
if (keys[' ']) // Если пробел нажат
{
gameover=FALSE; // gameover равно FALSE
filled=TRUE; // filled равно TRUE
level=1; // Стартовый уровень установим обратно в один
level2=1; // Отображаемый уровень также установим в один
stage=0; // Стадию игры установим в ноль
lives=5; // Количество жизней равно пяти
}
}
Код ниже проверяет, равен ли флаг filled
ИСТИНА (означает, что сетка была заполнена). Переменная filled
может быть установлена в ИСТИНУ одним из двух путей. Или сетка заполнена полностью
и filled равно ИСТИНА, когда игра закончена,
а пробел был нажат, чтобы перезапустить ее (код выше).
Если filled равно ИСТИНА, вначале
мы проигрываем крутую мелодию завершения уровня. Я уже объяснял, как работает PlaySound(). На сей раз, мы будем
проигрывать файл Complete.WAV
из каталога DATA. Снова, мы используем SND_SYNC для реализации задержки
перед запуском следующей стадии.
После того, как звук был проигран, мы увеличиваем stage
на один, и проверяем, что stage не больше чем
3. Если stage больше чем 3, мы устанавливаем
stage в 1, и увеличиваем внутренний уровень и
видимый уровень на один.
Если внутренний уровень больше чем 3, мы устанавливаем внутренний уровень (level) равным 3, и увеличиваем lives на 1. Если Вы достаточно быстры, и закончили уровень с 3, Вы заслуживаете бесплатную жизнь :). После увеличения жизней мы проверяем, что игрок не имеет больше чем 5 жизней. Если жизней больше чем 5, мы сбрасываем число жизней до 5.
if (filled) // Если сетка заполнена?
{
PlaySound("Data/Complete.wav", NULL, SND_SYNC); // Играем звук завершения уровня
stage++; // Увеличиваем Stage
if (stage>3) // Если Stage больше чем 3?
{
stage=1; // Тогда Stage равно 1
level++; // Увеличим уровень
level2++; // Увеличим отображаемый уровень
if (level>3) // Если уровень больше чем 3?
{
level=3; // Тогда Level равно 3
lives++; // Добавим игроку лишнюю жизнь
if (lives>5) // Если число жизней больше чем 5?
{
lives=5; // Тогда установим Lives равной 5
}
}
}
Затем мы сбрасываем все объекты (такие как игрок и враги). При
этом игрока помещаем снова в левый верхний угол сетки, а противникам присваиваются
случайные позиции на сетке.
Мы создаем два цикла (loop1 и loop2) для обхода
сетки. В них мы присваиваем значения всем вертикальным и горизонтальным линиям
в ЛОЖЬ. Если бы мы этого не делали, то, когда была бы запущенна следующая стадия,
то игра бы думала, что сетка все еще заполнена.
Заметьте, что код, который мы используем, чтобы очистить сетку, похож на код, который мы используем, чтобы вывести сетку. Мы должны проверить, что линии не будут рисоваться за правым и нижним краем. Именно поэтому мы проверяем, что loop1 меньше чем 10 прежде, чем мы сбрасываем горизонтальные линии, и мы проверяем, что loop2 меньше чем 10 прежде, чем мы сбрасываем вертикальные линии.
ResetObjects(); // Сброс позиции Игрока / Противника
for (loop1=0; loop1<11; loop1++) // Цикл по X координатам сетки
{
for (loop2=0; loop2<11; loop2++) // Цикл по Y координатам сетки
{
if (loop1<10) // Если X координата меньше чем 10
{
hline[loop1][loop2]=FALSE; // Задаем текущее горизонтальное значение в FALSE
}
if (loop2<10) // Если Y координата меньше чем 10
{
vline[loop1][loop2]=FALSE; // Задаем текущее вертикальное значение в FALSE
}
}
}
}
Теперь мы проверяем, попал ли игрок в песочные часы. Если точная
позиция fx игрока равна
позиции x песочных часов умноженная на 60, и
точная позиция fy игрока равна позиции y
песочных часов умноженная на 40, и hourglass.fx
равно 1 (т.е. песочные часы есть на экране), то тогда код ниже будет выполнен.
Первая строка кода - PlaySound("Data/freeze.wav",NULL,
SND_ASYNC | SND_LOOP).
В этой строке проигрывается файл freeze.WAV из каталога DATA. Обратите внимание на то, что мы на этот раз используем
SND_ASYNC. Мы хотим, чтобы звук замораживания играл без остановки игры.
Флаг SND_LOOP позволяет циклично повторять звук, пока мы не сообщим,
что пора прекратить играть, или пока не будет запущен другой звук.
После того, как мы запустили проигрывание звука, мы задаем hourglass.fx
в 2. Когда hourglass.fx равно 2, песочные часы исчезнут, враги замрут, и звук будет непрерывно
играть.
Мы также устанавливаем hourglass.fy в 0. Переменная hourglass.fy - счетчик. Когда она достигнет некоторого значения, значение переменной hourglass.fx изменится.
// Если игрок попал в песочные часы и они на экране
if ((player.fx==hourglass.x*60) && (player.fy==hourglass.y*40) && (hourglass.fx==1))
{
// Играть звук замораживания
PlaySound("Data/freeze.wav", NULL, SND_ASYNC | SND_LOOP);
hourglass.fx=2; // Задать hourglass fx значение 2
hourglass.fy=0; // Задать hourglass fy значение 0
}
В этой небольшой части кода увеличивает значение вращения игрока наполовину скорости выполнения игры. Если player.spin больше чем 360.0f, мы вычитаем 360.0f из player.spin. Это предохраняет значение player.spin от переполнения.
player.spin+=0.5f*steps[adjust]; // Вращение игрока по часовой
if (player.spin>360.0f) // Значение spin больше чем 360?
{
player.spin-=360; // Тогда вычтем 360
}
Код ниже уменьшает значение вращения песочных часов на 1/4 скорости выполнения игры. Если hourglass.spin меньше чем 0.0f, мы добавляем 360.0f. Мы не хотим, чтобы hourglass.spin принимало отрицательные значения.
hourglass.spin-=0.25f*steps[adjust]; // Вращение часов против часовой
if (hourglass.spin<0.0f) // spin меньше чем 0?
{
hourglass.spin+=360.0f; // Тогда добавим 360
}
В первой строке ниже увеличивается счетчик песочных часов, как
я говорил об этом. Переменная hourglass.fy увеличивается на скорость игры (она равна значению шага в зависимости
от значения корректировки).
Во второй линии проверяется, равно ли hourglass.fx значению 0 (не видимы) и счетчик песочных
часов (hourglass.fy) больше чем 6000 деленное на текущий внутренний уровень (level).
Если значение fx равно 0, и счетчик больше
чем 6000 деленное на внутренний уровень, то мы проигрываем файл hourglass.WAV из каталога DATA. Мы не хотим, чтобы игра остановилась,
поэтому мы используем SND_ASYNC. Мы не будем повторять звук на этот раз,
поэтому после того как звук проиграл, он не будет играть снова.
После того, как мы проиграли звук, мы задаем песочным часам
случайное положение по оси X. Мы добавляем единицу к случайному значению, для
того чтобы песочные часы не появились на стартовой позиции игрока в верхнем
углу сетки. Мы также задаем песочным часам случайное положение по оси Y. Мы
устанавливаем hourglass.fx в 1, это заставит песочные часы появиться на экране в этом новом
местоположении. Мы также сбрасываем hourglass.fy
в ноль, поэтому можно запустить счетчик снова.
Это приведет к тому, что песочные часы появятся на экране после заданного времени.
hourglass.fy+=steps[adjust]; // Увеличим hourglass fy
// Если hourglass fx равно 0 и fy больше чем 6000 деленное на текущий уровень?
if ((hourglass.fx==0) && (hourglass.fy>6000/level))
{
// Тогда играем звук песочных часов
PlaySound("Data/hourglass.wav", NULL, SND_ASYNC);
hourglass.x=rand()%10+1; // Случайная позиция часов по X
hourglass.y=rand()%11; // Случайная позиция часов по Y
hourglass.fx=1; // Задать hourglass fx значение 1 (стадия часов)
hourglass.fy=0; // Задать hourglass fy значение 0 (счетчик)
}
Если hourglass.fx равно нолю, и hourglass.fy больше чем 6000 деленное на текущий внутренний
уровнь (level), мы сбрасываем hourglass.fx
назад в 0, что приводит к тому, что песочные часы исчезают. Мы также устанавливаем
hourglass.fy в 0, потому что можно начать счет снова.
Это приводит к тому, что песочные часы исчезнут, если Вы не получаете их после некоторого времени.
// Если hourglass fx равно 1 и fy больше чем 6000 деленное на текущий уровень?
if ((hourglass.fx==1) && (hourglass.fy>6000/level))
{
hourglass.fx=0; // Тогда зададим fx равным 0 (Обратим часы в ноль)
hourglass.fy=0; // Задать fy равным 0 (Сброс счетчика)
}
Теперь мы проверяем, окончилось ли время 'замораживания противников'
после того, как игрок коснулся песочных часов.
Если hourglass.fx равняется 2, и hourglass.fy больше чем 500 плюс 500 умноженное на текущий внутренний уровень, мы прерываем звук заморозки, который беспрерывно проигрывается. Мы прерываем звук командой PlaySound(NULL, NULL, 0). Мы устанавливаем hourglass.fx снова в 0, и hourglass.fy в 0. После присваивания fx и fy к 0 происходит запуск цикла работы песочных часов снова. Значение fy будет равняться 6000 деленное на текущий внутренний уровень прежде, чем песочные часы появятся снова.
// Переменная песочных часов fx равно 2 и переменная fy
// больше чем 500 плюс 500 умноженное на текущий уровень?
if ((hourglass.fx==2) && (hourglass.fy>500+(500*level)))
{
PlaySound(NULL, NULL, 0);// Тогда прерываем звук заморозки
hourglass.fx=0; // Все в ноль
hourglass.fy=0;
}
Последнее что надо сделать - увеличить переменную задержки. Если Вы помните, задержка используется, чтобы обновить передвижение и анимацию игрока. Если наша программа финишировала, нам надо уничтожить окно и произвести возврат на рабочий стол.
delay++; // Увеличение счетчика задержки противника
}
}
// Shutdown
KillGLWindow(); // Уничтожить окно
return (msg.wParam); // Выход из программы
}
Я потратил много времени при написании этого урока. Вначале
это был урок по линиям, а в дальнейшем он перерос в небольшую интересную игру.
Буду надеяться, если Вы сможете использовать то, что Вы узнали в этом уроке
в ваших проектах с OpenGL. Я знаю, что Вы часто просили
рассказать об играх на основе мозаики (tile). Отлично Вы не сможете сделать что-то более мозаичное,
чем это :). Я также получил много писем, в которых меня спрашивали, как сделать
точное по пиксельное рисование. Я думаю, что охватил и это :). Наиболее важно,
что этот урок не только преподает Вам новые сведения о OpenGL,
но также рассказывает Вам, как использовать простые звуки, чтобы добавить немного
возбуждения в ваши визуальные произведения искусства! Я надеюсь, что Вам понравился
этот урок. Если Вы чувствуете, что я неправильно прокомментировал кое-что или
что код мог быть лучше в некоторых разделах, пожалуйста, сообщите мне об этом.
Я хочу сделать самые хорошие уроки по OpenGL,
я могу и я заинтересованным в общении с вами.
Пожалуйста, обратите внимание, это был чрезвычайно большой проект.
Я пробовал комментировать все настолько ясно, насколько это возможно, но облекать
мысли в слова, не столь просто, как это кажется. Знать о том, почему это все
работает, и пробовать это объяснить — это совершенно разные вещи :). Если Вы
прочитали урок, и можете объяснить все лучше, или если Вы знаете, способы помочь
мне, пожалуйста, пошлите мне свои предложения. Я хочу, чтобы этот урок был прост.
Также обратите внимание, что этот урок не для новичка. Если Вы не читали предыдущие
уроки, пожалуйста, не задавайте мне вопросов. Большое спасибо.
© Jeff Molofee (NeHe)
7 февраля 2003 (c) Сергей Анисимов