Цветный режим
В режиме 13h (или 19 в десятичной нотации) экран состоит из 320 пикселей по горизонтали и 200 — по вертикали. В нем поддерживается одновременное отображение до 256 цветов.
Теперь мы приближаемся к пониманию истинной ценности режима 13h. Он является наиболее простым в программировании. Как вы заметили, все графические режимы (CGA, EGA, VGA, SVGA, XGA и другие) используют некоторую область памяти для представления битовой карты, выводимой на экран. Этот раздел памяти называется видеобуфером. Для режима 13h и VGA-карт видеобуфер начинается с адреса А000:0000 и простирается до адреса AOOO;F9FF. Если вы владеете шестнадцатеричной арифметикой, то легко увидите, что размер буфера равен 64000 байт. Умножив 320 на 200, мы получим тот же результат. Это значит, что каждый пиксель в режиме 13h представлен одним байтом. Этот замечательный факт заставляет меня любить VGA-карту. На рисунке 5.1 показано строение видеобуфера VGA.
Позже в этой главе мы узнаем, как адресовать видеобуфер и записывать в него пиксели. Сейчас же я хочу поговорить о другом. Может возникнуть вопрос: «А не является ли экран 320х200 слишком маленьким?» Ответом будет: «И да, и нет». Сегодня разрешение 320х200 устарело, но благодаря 256 цветам и правильному рендерингу оно и по сию пору выглядит восхитительно. Эти плюсы заставляют нас забыть о низком разрешении. Кстати, игрок никогда не должен даже подозревать, что программа работает в режиме с низкой разрешающей способностью экрана.
Аксонометрические преобразования
Мы уже говорили об этом раньше (в шестой главе, «Третье измерением). Для воспроизведения объектов на экране компьютера используются аксонометричес кая проекция и связанные с ней математические преобразования, так как эта проекция выглядит более реалистично, чем параллельная (ортогональная). При построении аксонометрической проекции значения координат Х и Y модифицируются с учетом координаты Z (то есть расстояния от наблюдателя до плоскости просмотра, которой в нашем случае является экран), создавая тем самым иллюзию реальной перспективы.
Для правильного аксонометрического преобразования трехмерного спрайта мы должны использовать его центр в качестве его позиции или локального центра координат.
Это весьма важно! Если мы будем за центр координат принимать верхний левый угол спрайта (как мы это делали раньше), то получим искаженную картину. Всегда в качестве локального центра координат используйте середину объекта. Необходимость этого вызвана тем, что образ должен масштабироваться равномерно от его центра, а не от верхнего левого угла. (Запомнили? Повторите. Отлично!)
Формула 8.1. Аксонометрическая проекция спрайта.
Математический аппарат аксонометрии прост: нам требуется знать только значение координаты Z и расстояние до объекта. Имея эти данные, мы получаем новое положение проекции спрайта следующим образом:
х_Projected = view_distance * х_sprite / z_sprite
у_Projected = view_distance * у_sprite / z_sprite
Такой метод проекции работает четко: спрайты перемещаются вдоль линий перспективы в окне просмотра. Однако есть одна тонкость, которую необходимо обсудить. Речь идет о том, что я назвал бы пространственным искажением. Оно возникает, когда вы смешиваете одну систему виртуальных объектов с другой. Представим, например, что вы помещаете трехмерный спрайт в пространство, созданное трассировкой лучей. По отношению к этому пространству спрайт будет передвигаться и масштабироваться некорректно из-за того, что спрайт и стены создавались на основе двух различных способов построения перспективы.
Решить эту проблему можно с помощью всего лишь одного- единственного умножения во время построения проекции. Мы должны умножить координаты спрайта на коэффициент масштаба внутренней размерности. Этот поправочный коэффициент отображает одно математическое пространство на другое. По существу, мы должны масштабировать пространство спрайтов таким образом, чтобы оно соответствовало пространству трассированных лучей (или любому другому пространству, в которое мы собираемся его поместить). Конеч но, существует строгий математический способ расчета этого коэффициента (если вы любите точность, напишите мне письмо, и я покажу вам, как это делается). Что же касается меня лично, то я просто играю с расстояниями до спрайтов, пока объекты не начнут выглядеть вполне приемлемо. Итак, мы в общих чертах разобрались с проецированием спрайта на экран. На всякий случай повторю еще раз:
В качестве начала координат мы используем центр спрайта;
Затем мы делим координаты спрайта Х и Y на координату 2;
После этого умножаем полученный результат на поправочный коэффициент, учитывающий расстояние до объекта и фактор внутреннего масштаба пространства.
Чем-то напоминает рецепт колдовского зелья, не так ли? Тем не менее, выполнив эти операции и подвигав спрайт по экрану, мы получим иллюзию если не трехмерного, то, по крайней мере, двух с половиной-мерного изображения.
Таким образом, правильно поместив на экран объект, мы должны еще научиться корректно масштабировать его размеры при отдалении (или приближении) спрайта от наблюдателя.
Алгоритм художника
Алгоритм Художника — это один из тех алгоритмов, которые могут создавать ощущение реальности. Основная идея Алгоритма Художника состоит в сортировке поверхностей таким образом, что при рендеринге это выглядит корректно. Наиболее просто этот алгоритм может быть реализован, когда каждая поверхность параллельна плану просмотра (то есть перпендикулярна лучу зрения). В этом случае нам достаточно отсортировать поверхности в порядке уменьшения значения координаты Z. Затем мы сначала нарисуем самую дальнюю поверхность, потом более близкую и т, д. Это достаточно специфичное условие, но и у него есть свои реализации.
Проблемы начинают возникать, когда поверхность не параллельна плану просмотра. При этом все/разбивается на куски и мы вязнем в выполняемых тестах. Это значит, что нам надо выполнить множество вычислений. Рисунок 6.13 показывает вид сверху вниз двух многоугольников в одном из самых неудачных случаев.
Безразлично, как мы будем сортировать эти многоугольники - по минимуму, максимуму или среднему значению Z - мы всегда получим неверный результат. Чтобы этого избежать, мы должны проделать пять тестов для каждой пары многоугольников.
Тесты выполняются только в том случае, если значения Z для двух многоугольников совпадают. Иначе, они могут рисоваться в любой последовательности.
и многоугольника 2, как это
Имеются ли общие значения Х-координат для многоугольника 1 и многоугольника 2, как это изображено на рисунке 6.14
§
Если нет, то с этой парой многоугольников все в порядке. Последовательность их рисования не играет роли, поскольку они не закрывают друг друга;
§ Если да, то перейти к Тесту 2.
Алгоритм Художника, Тест 2
Имеются ли общие значения Y-координат для многоугольника 1 и многоугольника 2, как это показано на рисунке 6.15?
§ Если нет, то их можно рисовать также в любом порядке;
§ Если да, то выполнить Тесты 3 и 4.
К этому моменту мы уже
К этому моменту мы уже практически полностью уверены в том, что многоугольники перекрывают друг друга. Единственное, что осталось проверить — форму многоугольников. Возможно, что благодаря наличию углублений перекрываются не сами многоугольники, а только описывающие их прямоугольники. Такая ситуация показана на рисунке 6.17.
Обработка такой ситуации настолько сложна, что в этом случае не существует каких-либо общих рекомендаций,
похожи, поскольку оба они
Тесты 3 и 4 похожи, поскольку оба они работают с отсекающими плоскостями. Чтобы понять тест, мысленно продолжите грани многоугольника в бесконечность в обоих направлениях, создавая плоскость. Это плоскость отсечения. Посмотрите на рисунок 6.16.
Чтобы выполнить этот тест, создайте плоскость отсечения для первого многоугольника и проверьте многоугольник 2, а затем проделайте все то же самое, но наоборот.
§ Если ни одна из построенных плоскостей отсечения не пересекает другой многоугольник, то они могут быть корректно отрисованы;
§ Иначе перейти к Тесту 5.
Алгоритм Z - буфера
Поскольку скорость и объем памяти ПК постоянно увеличивается, на смену Алгоритму Художника пришел Алгоритм Z-буфера. Этот алгоритм более прост в реализации, чем Алгоритм Художника. (Сегодня большинство высокопроизводительных графических систем и графических станций имеют аппаратную реализацию этого алгоритма, что избавляет от необходимости решать проблему удаления невидимых поверхностей самостоятельно).
Реализация Алгоритма Z-буфера проста. Все, что для этого нужно - сам 2-буфер, который имеет такой же объем, как и видеобуфер. В нашем случае это будет матрица целых чисел размером 320х200. Затем мы заполняем ее значениями Z-координат многоугольников, следуя таким правилам:
1. Для данного множества трехмерных поверхностей вычисляем их проекции на план просмотра, иначе - на экран. Чтобы нарисовать трехмерный многоугольник на плоском экране, мы должны спроецировать его на план просмотра, используя один из двух видов проекции, которые мы обсуждали ранее. После проецирования многоугольник будет обладать множеством вершин, являющихся точками на плоскости. Потом мы заполним многоугольник, используя эти точки для создания граней.
2. Определяем Х- и Y-компоненты для каждой точки (необходимо помнить, что может быть сколько угодно точек с одинаковыми значениями Х- и Y-координат и различными значениями Z.)
3. Затем используем уравнение плоскости для плоскости, общей с многоугольником, решая его относительно компонента Z для каждой пары Х- и Y-координат, после чего вычисляем значение Z для всех точек в границах многоугольника.
4. Записываем значение Z и цвет для каждой точки Z-буфера.
5. Затем мы смотрим, какая из точек будет нарисована на экране. Чтобы сделать это, найдем точку со значением Z-координаты, ближайшей к плану просмотра.
6. Рисуем пиксель с цветом данной точки.
Алгоритмы Поиска. Выслеживание игрока
«Мозги», которые мы сконструировали, получились достаточно умными, так что существо даже получило шанс выжить- И теперь я хочу поговорить о другой довольно простой вещи — как должен быть организован поиск.
Вспомним историю Тезея с Минотавром. Жил когда-то давным-давно Минотавр (человек с головой быка), который преследовал Тезея. В общем, Тезей украл у Минотавра... не помню точно — CD-ROM, или что-то там еще... Тезей попал в ловушку (в действительности, подрядчик, построивший ее, называл ее лабиринтом), но успел выбраться наружу прежде, чем Минотавр его настиг. Вопрос состоит в том, как нужно двигаться по лабиринту, чтобы найти путь к выходу или любой произвольной точке лабиринта, и при этом не застрять в нем навсегда? Давайте взглянем на рисунок 13.8, где изображен пример лабиринта. Существует простой алгоритм, обозначенный здесь как Алгоритм 13.7, который поможет вам найти выход из любого лабиринта. Будьте внимательны, в один прекрасный момент он может вам сильно пригодиться!
Алгоритм 13.7. Путь наружу.
do {
Двигайтесь вдоль правой стены
если встретился проход,
поверните направо
если попали в тупик,
развернитесь на 180 градусов
}пока не выйдете из лабиринта
Этот алгоритм работает безупречно: попробуйте его у себя дома. Стартуйте из любого угла вашей квартиры и следуйте алгоритму. Возможно, при известных обстоятельствах вы даже найдете путь к каждой двери.
Так как нас интересуют видеоигры, попытаемся использовать этот алгоритм, чтобы помочь созданиям в игре выследить игрока, не тыкаясь в стены (если мы, конечно, не хотим обманывать игрока, позволив созданиям беспрепятственно проходить сквозь стены). Использование этого алгоритма может привести к затруднениям, если надо обходить объекты круглой формы. Однако мы можем или избегать такой ситуации, или добавить в программу дополнительный алгоритм для предотвращения ходьбы по кругу.
Алгоритмы Преследования и Уклонения
Итак, начнем. Наиболее простыми игровыми алгоритмами искусственного интеллекта являются так называемый Алгоритм Преследования и его противоположность — Алгоритм Уклонения. В основном, они заставляют игровой объект или догонять игрока, или убегать от него. Конечно, конфигурация игрового пространства также должна учитываться, чтобы преследователь даже и не пытался пройти сквозь стены. Давайте рассмотрим процесс, моделирующий погоню некоторого существа за игроком.
АЛГОРИТМЫ, СТРУКТУРЫ ДАННЫХ И МЕТОДОЛОГИЯ ВИДЕОИГР
Пришло время поговорить о том, как же собрать воедино все, о чем рассказывалось в предыдущих главах этой книги, чтобы получить в результате настоящую компьютерную игру- Компьютерная игра —программа синергистическая, она не является просто совокупностью своих частей. Игра должна на время поместить игрока в другую реальность. Причем этот другой мир будет существовать только в компьютере и сознании игрока. Чтобы достигнуть этого мы должны понять, как зрительно и эмоционально воспринимает игру сам игрок, и на основании этого выработать подходящую концепцию архитектуры компьютерной игры. В этой главе мы затронем множеством, не всегда связанных между собой, но несомненно важных, если, конечно, вы все еще хотите превратить те смутные очертания, которые возникают в вашем воображении, в захватывающую компьютерную игру.
В этой главе много неочевидных выводов и намеков, поэтому читайте внимательно! Основные темы этой главы:
§
Структуры данных для представления игрового пространства;
§ Столкновения объектов;
§ Представление игровых объектов;
§ Структуры данных в компьютерных играх;
§ Клонирование игровых объектов;
§ Состояния объектов;
§ Пользовательский интерфейс;
§ Демонстрационные режимы;
§ Сохранение состояния игры;
§ Моделирование настоящего мира.
Анализ игры Net-Tank
Если вы обзовете Net-Tank пережитком каменного века, я полностью соглашусь с вами. Однако она содержит несколько интересных:технических; приемов, которые вы можете использовать (и которые в дальнейшем будут применены в Warlock'e). Вся игровая логика содержится в функции main() Си-программы, Я сделал это для того, чтобы легче было обозреть игру в целом. Исключение составляют только вызываемые функции, которые являются низкоуровневыми, но обычно их имена говорят о том, для чего они предназначены или что они делают (например, сложно не понять, что означает Draw_Sprite). Основная часть включает в себя пару сотен строк, и если вы поймете их смысл, вы в хорошей фopмe. Давайте разберем игру, рассматривая раздел за разделом.
Анимация
Анимация также очень эффектна для начала и конца игры. Вы можете использовать мультипликацию для привлечения внимания к торговой марке вашей фирмы, для показа фабулы игры или для завершения игры с предварительным просмотром того, что происходило. Наиболее общим форматом для анимации является FLI- или FLC-форматы. Формат FLI имеет разрешение 320х200, что вполне достаточно для большинства условно-бесплатных игр. Формат FLC был разработан несколько позднее и поддерживает любое необходимое разрешение.
«Animotion»
Animation (animation (мультипликация )+ motion (движение)) — это придуманное мною слово, которое должно войти в словарь терминов компьютерных игр. (Дальше я тоже буду выдумывать слова, но они уже не будут так хороши как это.) Оно описывает точное слияние анимации (мультипликации) и движения.
В игре Tombstone четвертой главы, "Механизмы двухмерной графики", маленький ковбой гуляет по улице. В действительности это больше похоже на то, что он одновременно ковыляет и совершает дикие прыжки (если такое вообще можно представить). Вся проблема заключается в принципе его движения, которое не было синхронизировано с мультипликационными кадрами. Многие объекты, в компьютерных играх имеют постоянную
скорость. Такие игровые объекты как ракета и прочие летающие предметы — хороший тому пример, Однако к имеющим под собой опору (например, землю) объектам, которые должны выглядеть реально, нужно применять совсем другой подход.
«Animotion» абсолютно необходим, если вы хотите, чтобы движение -ходьба, бег или прыжки — выглядели реалистично. Иначе мультипликационные объекты выглядят неестественно. Мы не можем просто в цикле менять мульти пликационный кадр и одновременно передвигать объект на произвольное расстояние. Мы должны рассмотреть каждый кадр и определить, на сколько в действительности следует перемещать объект для этого кадра. Затем мы создаем таблицу выбора для движения, в которой в качестве индекса используем номера кадров. В этой таблице будут содержаться величины перемещений, которые и будут использоваться при оживлении картинки.
На рисунке 7.6 изображены мультипликационные кадры движения худого человечка, которого-мы назовем «человечек-палка».
(У «человечка-палки» серьезные проблемы со зрением — он имеет только один глаз.) Для движения человечка у нас будет 12 мультипликационных кадров. Для каждого из кадров я прикинул и задал величину перемещения, при котором движение становится действительно похожим на прогулку. Немного помучавшись, я получил для каждого мультипликационного кадра значения, приведенные в таблице 7.3.
Таблица 7.3. Таблица перемещений.
Кадр
Значение
Кадр
Значение
Кадр
Значение
1
17
5
3
9
6
2
0
6
0
10
2
3
6
7
17
11
3
4
2
8
0
12
0
Листинг 7.7. Демонстрация «animotion» (STICK.С).
// ВКЛЮЧАЕМЫЕ ФАЙЛЫ ////////////////////////////////////////
#include <io.h>
#include <conio.h>
#include <stdio.h>
#include <stdlib.h>
#include <dos.h>
#include <bios.h>
#include <fcntl.h>
#include <memory.h>
#include <malloc.h>
#include <math.h>
#include <string.h>
#include "graph0.h" // включаем нашу графическую библиотеку
// определения /////////////////////////////////////////////
#define VEL_CONST -1 // флаг постоянной скорости перемещения
// ГЛОБАЛЬНЫЕ ПЕРЕМЕННЫЕ ///////////////////////////////////
unsigned int far *clock = (unsigned int far *)0x0000046C;
// указатель на внутренний таймер 18.2 "тик"/с
sprite object;
pcx_picture stick_cells, street_cells;
// таблица выбора содержит величины перемещения для каждого
// мультипликационного кадра, чтобы движение было более реалистичным
int object_vel = {17,0,6,2,3,0,17,0,6,2,3,0};
// функции ////////////////////////////////
void Timer (int clicks)
{
// эта функция использует внутренний таймер с частотой 18.2 "тик"/с
// 32-битовое значение этого таймера находится по адресу 0000:046Сh
unsigned int now;
// получаем текущее время
now = *clock;
//Ожидаем до истечения указанного периода времени.
// Заметьте, что каждый "тик"' имеет длительность примерно в 55 мс
while(abs(*clock - now) < clicks)() {}
// конец Timer
// ОСНОВНАЯ ПРОГРАММА //////////////////////////////////////
void main(void) {
int index, done=0,
vel_state=VEL_CONST;
// установка видеорежима 320х200х256
Set_Mode(VGA256);
// установка размера для системы отображения спрайта
sprite_width = 32;
sprite_height =64;
// инициализация
файла PCX, который
содержит изображение
улицы
PCX_Init((pcx_picture_ptr)&street_cells) ;
// загрузка файла PCX, который содержит изображение улицы
PCX__Load("street.pcx", (pcx_picture_ptr)&street_cells,1} ;
PCX_Show_Buffer((pcx_picture_ptr)&street_cells) ;
// используем буфер PCX
как дублирующий
double_buffer = street_cells.buffer;
Sprite_Init((sprite_ptr)&object,0,0,0,0, 0,0);
// инициализация
файла PCX, который
содержит кадры
спрайта
PCX_Init((pcx_picture_ptr)&stick_cells);
// загрузка файла PCX, который содержит кадры спрайта
PCX_Load("stickman.pcx", (pcx_picture_ptr) &stick_cells,1) ;
// выбираем 6 кадров
движения
PCX_Grap_Bitmap((pcx_picture_ptr)&stick_cells,
(sprite_ptr)&object,0,0,0);
PCX_Grap_Bitmap((pcx_picture_ptr)&stick_cells,
(sprite_ptr)&object,1,1,0);
PCX_Grap_Bitmap( (pcx_picture_ptr) &stick_cells,
(Sprite_ptr)&object,2,2,0);
PCX_Grap_Bitmap((pcx_picture_ptr)&stick_cells,
(sprite_ptr)&object,3,3,0);
PCX_Grap_Bitmap((pcx_picture_ptr)&stick cells,
(sprite_ptr)&object/4,4,0);
PCX_Grap_Bitmap( (pcx_picture_ptr) &stick_cells, (sprite_ptr) &object, 5, 5, 0);
PCX_Grap_Bitniap ( (pcx_picture_ptr) &stick_cells,(sprite_ptr)&object,6, 0,1);
PCX_Grap_Bitmap((pcx_picture_ptr)&stick_cells,
(sprite_ptr)&object,7, 1,1);
PCX_Grap_Bitmap ( (pcx_picture_ptr) &stick_cells,
(sprite_ptr)&object,8, 2,1) ;
PCX_Grap_Bitmap((pcx_picture_ptr)&stick_cells,
(sprite_ptr) &object, 9, 3,1);
PCX_Grap_Bitmap((pcx_picture_ptr)&stick_cells,(sprite_ptr)&object,10,4,1);
PCX_Grap_Bitmap((pcx_picture_ptr)&stick_cells,
(sprite_ptr)&object,11,5,1);
// файл stickman.pcx больше не нужен
PCX_Delete((pcx_picture_ptr)&stick_cells);
// настраиваем параметры человечка
object.x = 10;
object.у = 120;
object.curr_frame = 0;
// сохраняем
фон
Behind_Sprite((sprite_ptr)&object);
// главный цикл
while(!done)
{
// стираем спрайт
Erase_Sprite((sprite_ptr)&object) ;
// увеличиваем номер кадра на единицу
if (++object.curr_frame > 11) object.curr_fcame = 0;
// перемещаем спрайт, используя или постоянную скорость,
// или
таблицу выбора
if (vel_state==VEL_CONST)
{
object.x+=4;
}
// конец if
else
/ {
// используем номер кадра для определения величины
// перемещения по таблице выбора
object.x += object_vel[object.curr_frame];
} // конец else
}
Вы наверняка обратили внимание на горы и местность за окнами, которые перемещаются при ваших поворотах. Фактически, этот пейзаж есть не что иное, как плоское изображение, прокручиваемое как фон. Эти изображения, чтобы они выглядели трехмерными, прорисовываются специальным образом, но, тем не менее, в основе их перемещения лежит все та же прокрутка двухмерного изображения.
Персональные компьютеры могут иметь специальное аппаратное обеспечение, облегчающее прокрутку. Некоторые карты VGA имеют до мегабайта оперативной памяти, что дает возможность рисовать мир вашей игры прямо в видеобуфере, поручая производить прокрутку самой видеокарте. Однако существует две проблемы:
Во-первых, если мы будем полагаться на определенную аппаратуру, наши программы станут аппаратнозависимыми и не будут работать на картах VGA с меньшей памятью;
Во-вторых, прокрутка в режиме 13h существенно сложнее, чем прокрутка в режимах EGA, так как видеопамять в этом случае не разбита на несколько плоскостей. Эта означает, что мы не можем использовать в данном режиме аппаратную прокрутку, таким же образом как в режимах EGA.
Таким образом, мы не будем использовать для прокрутки аппаратное обеспечение персонального компьютера. Мы будем это делать программно, применяя блочное копирование из памяти в видеобуфер. Используя этот метод, мы будем абсолютно независимыми от аппаратного обеспечения, следовательно наши программы будут более гибкими.
Прокрутку целого экрана или его части можно осуществлять двумя путями:
§ Можно нарисовать все пространство игры в обширном буфере памяти. Однако шесть предварительно нарисованных экранов займут 6х64000 байтов, то есть 384К. Это довольно большие потери памяти. В любом случае, как мы только что говорили, мы должны будем сделать воображаемое окно, перемещающееся по этому буферу и отображать все, что в него попадает, на экран; |
§ Второй метод более медленный, но требует меньше памяти. Он основан на генерации изображения «на лету». Под выражением "на лету" я подразумеваю, что мир будет представляться с помощью иначе структурированных данных - например, в виде двухмерной матрицы, где каждая ячейка 8х8 пикселей ставится в соответствие растровому изображению части игрового пространства. Такой ячеистый мир в шесть экранов будет занимать всего 6000 байт. При перемещении окна по ячейкам матрицы, на экране будут воспроизводиться соответствующие растровые изображения.
Главный недостаток этого метода — скорость. Ведь для каждой ячейки нужно будет найти соответствующее растровое изображение и перенести его в дублирующий буфер или на экран, поэтому на перерисовку экрана будет уходить много времени. Вы наверняка встречали игры, которые выглядят не очень симпатично во время прокрутки из-за того, что перерисовка экрана явственно заметна.
Это как раз и происходит из- за применения второго метода. Мы должны принести в жертву либо время, либо память. Выбор как всегда за вами.
Чтобы показать вам пример скроллинга, я написал программу, которая создает игровое пространство размером 640х100 пикселей. Я двигаю окно по изображению мира игры и передаю его содержимое в середину экрана. В этой игре мир состоит из звезд и гористого горизонта. (Этот пейзаж немного напоминает игру Defender.) Перемещая с помощью клавиатуры окно просмотра вправо и влево, вы можете прокрутить весь пейзаж. Листинг 7.8 содержит текст этой программы, которая называется DEFEND.С.
Листинг 7.8. Пример прокрутки (DEFEND.C).
// ВКЛЮЧАЕМЫЕ ФАЙЛЫ ////////////////////////////////////////
#include <stdio.h>
#include <math.h>
#include <graph.h>
#inciude <malloc.h>
#include <memory.h>
#include <string.h>
// ОПРЕДЕЛЕНИЯ ///////////////////////////////////////////
#define SCREEN_WIDTH (unsigned int)320
#define SCREEN_HEIGHT (unsigned int)200
// ГЛОБАЛЬНЫЕ
ПЕРЕМЕННЫЕ
////////////////////////////////////////
unsigned char far *video_buffer = (char far *)0xA0000000L;//указатель
на видеобуфер
unsigned char far *double_buffer = NULL;
// ФУНКЦИИ /////////////////////////////////////////////////
void Show_View_Port(char far *buffer,int pos)
{
// Копирование части дублирующего буфера на экран
unsigned int y,double_off, screen_off;
// нужно переместить 100 строк, перемещаем их построчно
for (y=0; у<100; у++) {
// расчет начального смещения дублирующего буфера
//у
* 640 +pos
double_off = ((у<<9) + (у<<7) + роs);
// расчет начального смещения в видеобуфере
// у * 320 + 80
screen_off = (((у+50)<<8) + ((у+50)<<6) + 80);
// перемещение данных
_fmemmove ((char far *)&video_buffer[screen off],
(char far *)&double_buffer[double_off],160);
} // конец
цикла for
} // конец Show View_Port ////////////////////////////////////////////////////////////////
void Plot_Pixel_Fast_D2(int x,int y,unsigned char color)
{
// прорисовка пикселей в дублирующем буфере нашего виртуального
// экрана размером 640х100 пикселей
// учтем, что 640*у = 512*у + 128*у = у<<9 + у<<7
double_buffer[ ((у<<9) + (у<<7)) + х] = color;
} // конец Plot_Pixel_Fast_D2 ////////////////////////////////////////////////////////////
void Draw_Terrain(void) {
// эта функция рисует ландшафт в дублирующем буфере
// размером 640х100 пикселей
int х,у=70,index;
// очистка
памяти
_fmemset(double_buffer,0,(unsigned int}640*(unsigned int)100);
// рисуем
звезды
for (index=0; index<200; index++)
{
Plot_Pixel_Fast_D2(rand()%640,rand()%70,15);
} // конец цикла for
//рисуем горы
for (x=0; x<640; х++)
{
// расчет смещения
y+=-1 + rand()%3;
// проверяем, находятся ли горы в приемлемых границах
if (y>90) у=90;
else
if (y<40) у=40;
// рисуем точку в дублирующем буфере
Plot_Pixel_Fast_D2 (x,y, 10);
} // конец цикла for } // конец Draw_Terrain
// ОСНОВНАЯ ПРОГРАММА //////////////////////////////////////
void main(void)
{
int done=0,sx=0;
// установка видеорежима 320х256х256
_setvideomode(_MRES256COLOR);
_settextposition(0,0);
printf("Use < > to move. Press Q to quit.");
// рисуем небольшое окно
_setcolor(l);
_rectangle(_GBORDER, 80-1,50-1,240+1,150+1) ;
// резервируем память под дублирующий буфер
double_buffer=(char far *)_fmalloc(SCREEN_WIDTH*SCREEN_HEIGHT+1);
Draw_Terrain() ;
Show_View_Port(double_buffer,sx) ;
// главный цикл
while (!done)
{// не Нажал ли игрок клавишу?
if (kbhit())
switch(getch())
{
case ',': // переместить окно влево, если это возможное
{
sх-=2;
if (sx<0)
sx=0;
} break;
case '.':// переместить окно вправо если это возможно
{
sx+=2;
if (sx > 640-160)
sx=640-160;
} break;
case 'q': // игроку
надоело?
{
done=1; } break;
} // конец
оператора
switch
// копируем окно просмотра на экран
Show_View_Port(double_buffer,sx);
_settextposition(24,0);
printf("Viewport position = %d ",sx);
} // конец оператора if
} // конец оператора while
// восстановление видеорежима
_setvideomode(_DEFAULTMODE);
}// конец функции main
Если вы похожи на меня, то, вероятно, захотите написать игру типа Defender. Отлично! Отложите книгу — и вперед. Не беспокойтесь, я подожду. По крайней мере, попытайтесь в своей программе заставить джойстик управлять полетом корабля.
О, вы уже вернулись? Тогда поговорим о тех специальных потрясающих эффектах, без которых ни одна игра никогда не завоюет популярности.
API пакета программ DIGPAK
Пакет программ DIGPAK наряду с интерфейсами реального и защищенного режима DOS включает в себя набор исходных текстов, которые работают во всех моделях памяти. Драйверы DIGPAK полностью совместимы с драйверами MIDPAK. Кроме того, хотя все драйверы DIGPAK разрабатывались для реального режима, они работают в защищенном режиме через подключаемый интерфейс DIGPLAY. Поддержка защищенного режима требует наличия драйверов DIGPAK версии 3.4 или выше.
В версии 3.4 пока имеются не всё драйверы звука DIGPAK. Еще не конвертированы драйверы для Gravis Ultrasound и Turtle Beach Multisound. Кроме того, драйверы, использующие таймер 8253, также не будутработать в
защищенном режиме из-за большой потери производительности и возможных конфликтов вследствие высокой частоты прерываний таймера.
Набор драйверов оцифрованного звука использует вектор прерывания 66h, что обеспечивает прозрачный программный интерфейс. Ниже описываются два способа воспроизведения оцифрованного звука. Первый способ основан на прерываниях. Второй — на использовании библиотеки функций на Си или . ассемблере, которые позволяют не только осуществлять доступ к драйверам звука, но и предоставляют другие полезные функции. Эти функции находятся в исходном файле DIGPLAY.ASM.
******************************************************************
********* Спецификация интерфейса прерывания 66h
*****************
******************************************************************
Вызывая функции для исполнения звука, вы передаете ей адрес структуры звуковых данных (SNDSTRUC), содержащей базовую информацию, описывающую желаемый звуковой эффект. Ниже показано, как это сделать.
*** РЕАЛЬНЫЙ РЕЖИМ:
typedef struct
(
unsigned char far *sound* // дальний указатель на звуковые данные
unsigned short sndlen; // длина звуковой последовательности
short far *IsPiaying; // адрес флага состояния
short frequency; // частота воспроизведения
} SNDSTRUC;
*** ЗАЩИЩЕННЫЙ РЕЖИМ:
typedef struct
{
unsigned char *sound* // должен
быть в
формате
// СМЕШЕНИЕ:СЕГМЕНТ в пределах 1Мб!
unsigned short sndlen;// длина звуковой последовательности < 64К
short *IsPlaying; // адрес флага состояния. СМЕЩЕНИЕ:СЕГМЕНТ!
short frequency; // частота воспроизведения
} SNDSTRUC;
********* функции DIGPAK *****************************************
API пакета программ MIDPAK
Драйвер MIDI, MIDPAK использует вектор прерывания 66h, что обеспечивает прозрачный программный интерфейс. Ниже.описываются два способа воспроизведения MIDI-музыки. Первый способ основан на прерываниях. Второй — на использовании библиотеки функций на Си или ассемблере, которые позволяют не только осуществлять доступ к драйверам звука, но и предоставляют другие полезные функции. Эти функции находятся в исходном файле MIDPACK.ASM.
MIDPAK использует тот же вектор прерывания, что и DIGPAK. Пакет DIGPAK описывает полный набор драйверов оцифрованного звука, поставляемый фирмой Audio Solution. MIDPAK полностью совместим с DIGPAK. Если ваша программа должна воспроизводить как MIDI-музыку, так и оцифрованный звук, надо вначале загрузить требуемый драйвер оцифрованного звука, и затем поверх него загрузить MIDI-драйвер MIDPAK. Драйвер MIDPAK обнаруживает присутствие драйвера DIGPAK и перенаправляет через него все вызовы. Если аппаратное обеспечение не в состоянии независимо воспроизводить оцифрованный звук (поддержка прямого доступа в память: Sound Blaster и ProAudio Spectrum), тогда во время воспроизведения оцифрованного звука исполнение MIDI-музыки будет выключено. Воспроизведение MIDI-музыки возобновится сразу же после окончания проигрывания оцифрованного звукового фрагмента. Для прикладной программы этот процесс полностью прозрачен.
Пакет программ MIDPAK использует набор звуковых драйверов MIDI, разработанных Miles Design Inc. Эти драйверы различаются по своему размеру и имеют расширение .ADV. При старте MIDPAK всегда загружает драйвер MUSIC.ADV. Поэтому прикладная программа перед загрузкой MIDPAK должна переименовать необходимый драйвер в MUSIC.ADV.
MIDPAK не исполняет непосредственно файлы MIDI. Вы должны конвертировать файлы MIDI (с расширением .MID) в файлы обобщенного MIDI (-XMI), используя программу MIDIFORM или утилиту MENU. Драйверы Расширенного MIDI фирмы Miles Design Incorporated поддерживают каналы 2-9 для мелодических инструментов и канал 10 для ударных.
Замечание
Любой не указанный номер функции является устаревшим или не используется.
Функция № 1: UnloadMidPak
Эта функция освобождает память, занятую резидентной частью MIDPAK, и на должна использоваться прикладной программой! Используется MIDPAK для внутренних целей и приведена здесь только для полноты картины.
ВХОД: AX=700h Номер команды.
ВЫХОД: Ничего
Функция № 2: DigPakAvailable
Функция определяет доступность драйвера DIGPAK под драйвером MIDPAK.
ВХОД: AX=701h Номер команды.
ВЫХОД: АХ=0 DIGPAK не доступен.
АХ=1 DIGPAK доступен.
Функция № 3 PlaySequence
Функция исполняет последовательность из текущего зарегистрированного XMIDI-файла.
ВХОД: AX=702h Номер команды,
BX=SEQ Номер последовательности, начиная с нуля,
ВЫХОД: АХ=1 Последовательность проигрывается.
АХ=0 Последовательность не доступна.
Функция № 4: SegueSequence
Функция регистрирует с указанием кода активации новую последовательность Для исполнения по триггерному событию. Если значение кода активации -1, то переход к данной последовательности будет осуществлен по ближайшему триггеру. Триггер с указанием кода события помещается в поток данных MIDI с помощью Контроллера 119. Контроллеры 119 могут быть помещены в любое место потока данных MIDI для передачи программе информации о текуацей позиций в MIDI-последовательности.
ВХОД: AX=703h Номер команды.
ВХ = SEQ Номер регистрируемой последовательности.
СХ= ACT Код активации события, -1 означает следующий триггер.
Функция № 5: RegisterXmidi
Функция регистрирует адрес файла XMIDI для исполнения.
ВХОД: AX=704h Номер команды,
BX=Offset Смещение в дальнем адресе данных XMIDI.
CX=Segment Сегмент в дальнем адресе данных XMIDI.
SI=Low len Младшее слово значения длины данных XMIDI.
DI=High len Старшее слово значения длины данных XMIDI.
ВЫХОД: АХ=0 Ошибка регистрации данных XMIDI.
АХ=1 Файл XMIDI зарегистрирован резидентно. Это означает, что файл полностью поместился во внутренний буфер MIDPAK. Ваша программа может освободить память, связанную с файлом XMIDI, так как MIDPAK создал для себя его, копию. Это очень полезно в средах с виртуальной памятью, где прикладная программа не всегда имеет фиксированный адрес в памяти. Это также позволяет MIDPAK исполнять музыку в фоновом режиме под DOS.
АХ=2 Файл XMIDI зарегистрирован. Прикладная программа ответственна за то, чтобы указанный фиксированный адрес в памяти всегда содержал соответствующие данные XMIDI.
Функция № 6: MidiStop
Функция, останавливает воспроизведение текущей последовательности MIDI.
ВХОД: AX=705h Номер команды.
ВЫХОД: Ничего
Функция № 8: ReportTriggerCount
Функция возвращает счетчик триггерных событий и код последнего события.
ВХОД: AX=707h Номер команды.
ВЫХОД: AX=COUNT Количество событий со времени последнего сброса счетчика.
DX=ID Код последнего события. Коды событий вы можете найти в спецификации XMIDI..
Функция № 9: ResetTriggerCount
Функция сбрасывает счетчик событий в ноль.
ВХОД: AX=708h Номер команды.
ВЫХОД: Ничего
Функция № 12: ResumePlaying
Функция продолжает исполнение остановленной последовательности.
ВХОД: АХ-70Вh Номер команды.
Функция № 13: SequenceStatus
Функция возвращает состояние последовательности.
ВХОД: АХ=70Сh Номер команды.
ВЫХОД: АХ=Статус
SEQ_STOPPED 0 Воспроизведение последовательности остановлено.
SEQ_PLAYING 1 Последовательность исполняется в настоящий момент.
SEQ_DONE 2 Исполнение последовательности завершено.
Функция № 14: RegisterXmidiFile
Функция регистрирует файл по его имени.
ВХОД: AX=70Dh Номер команды.
BX=Offset Смещение адреса имени файла.
CX=Segment Сегмент адреса имени файла.
Функция № 15: RelativeVolume
Функция возвращает относительную громкость музыки в процентах.
ВХОД: АХ=70Еh Номер команды.
ВЫХОД: АХ= VOL Текущая относительная громкость в процентах 0-100.
Функция № 16: SetRelativeVolume
Функция устанавливает относительную громкость в процентах.
ВХОД: AX=70Fh Номер команды.
Функция № 17: BootstrapMidPak
Функция позволяет приложению устанавливать драйвер MIDPAK.
ВХОД: АХ-710h Номер команды.
ВХ:СХ СЕГМЕНТ:СМЕЩЕНИЕ драйвера ADV.
DX:SI СЕГМЕНТ:СМЕЩЕНИЕ файла AD.
Функция № 18: PollMidPak
ВХОД; AX=710h
Функция используется в сочетании с PMIDPAK.COM. Это версия MIDPAK, работающая по опросу. Обычный MIDPAK перехватывает прерывание либо от таймера, либо от часов реального времени и обслуживает его с частотой 120 раз в минуту. Однако некоторые сложные прикладные программы сами обслуживают аппаратные прерывания и требуют исполнения фоновой музыки с иной частотой дискретизации, или синхронизируют по таймеру графические функции, чтобы избежать возможного прерывания музыкой графических процедур.
После того как MIDPAK установлен, он восстанавливает вектор прерывания таймера и не проявляет себя до- тех пор, пока программа, не выполнит прерывание 66h с командой 0711h. Вы должны вызывать это прерывание с частотой 120 раз в минуту или с частотой дискретизации, указанной при запуске MIDIFORM. При запуске MIDIFORM вы можете указать частоту дискретизации для вашей музыки, отличную от принятого по умолчанию значения 120. При снижении частоты дискретизации вы услышите ухудшение качества музыки, так как ASDR не будет реагировать достаточно быстро. Снижение ее, например, до 60 раз в минуту не будет сильно заметным, однако уменьшение частоты до 30 или 15 вызовет значительное ухудшение качества звучания. Очевидно, что исполнение многоканальной MIDI-музыки на таком частотном синтезаторе, как Adiib, потребует определенных ресурсов процессора. Переквантовывая свою музыку и задавая MIDPAK удобные для вас частоты, вы можете добиться хорошего баланса использования ресурсов компьютера.
Функция № 19: MidpakClock
Функция возвращает текущее значение счетчика MIDPAK.
ВХОД: AX=7l2h
ВЫХОД: AX:DX Текущее значение счетчика с момента старта MIDPAK. Счетчик обновляется 120 раз в минуту, и ваше приложение может использовать его как таймер.
ВХ:СХ Образует дальний указатель на счетчик.
ВХ Смещение, СХ Сегмент.
Функция возвращает значение внутреннего счетчика MIDPAK. При старте счетчик, представляющий собой двойное слово, имеет нулевое значение, которое увеличивается каждую минуту на 120, и ваше приложение может использовать его как таймер, просто опрашивая эту функцию.
Функция № 20: TriggerCountAddress
Функция возвращает адрес размещения счетчика триггеров.
ВХОД: АХ=713h
ВЫХОД: AX:DX Образует адрес целого значения счетчика триггеров. Значение этого счетчика увеличивается на единицу при каждом появлении Контроллера 119 в файле MIDI.
Функция № 21: EventIDAddress
Функция возвращает адрес размещения кода события.
ВХОД: AX=714h
ВЫХОД: AX:DX Образует адрес целого значения кода события. В этой ячейке памяти хранится код последнего события вызванного Контроллером 119.
Функция № 23: ReportSequenceNumber
Функция возвращает номер исполняемой в данный момент последовательности.
ВХОД: AX=716h
ВЫХОД: Возвращает номер исполняемой в данный момент последовательности.
extern short cdecl CheckMidiIn (void);
// Возвращает 1, если MIDPAK
установлен, 0 - если нет.
extern short cdecl DigpakAvailable (void) ;
// Возвращает 1, если DIGPAK
установлен, 0 - если нет. /****************************•*************************************
/** Эти флаги возвращает функция регистрации данных XMIDI ***
/****************************************************************/
#define FAILURE_TO_REGISTER 0 // Ошибка
регистрации
XMIDI файла.
#define REGISTERED RESIDENT 1 // Резидентный драйвер полностью
// содержит данные XMIDI. Приложение
// может освободить память, которую
// они занимали.
#define REGISTERED_APPLICATION
1 //Драйвер не имеет настолько
// большого буфера, чтобы полностью
// загрузить в него данные XMIDI.
// Приложение обязано обеспечить
// сохранение в памяти по указанному
// адресу данные XMIDI.
extern short cdecl PlaySequence (short seqnum);
// исполняет последовательность с данным номером
// из зарегистрированного файла XMIDI
#define NEXT_CALLBACK
- 1 // активизация по ближайшему событию
extern short cdecl SegueSequence (short seqnum, short activate) ;
// Переключает исполнение последовательности на
// указанную последовательность по наступлению
// события с кодом, равным указанному коду активации.
// Если код активации равен -1, переключение
// произойдет по ближайшему событию.
extern short cdecl RegisterXmidi (char *xmidi, long int size) ;
// Регистрирует XMIDI файл для воспроизведения.
// Этот вызов зарегистрирует все последовательности.
extern short cdecl MidiStop (void);
// остановка воспроизведения текущей последовательности
extern long int cdecl ReportCallbackTrigger (void);
// младшее слово - счетчик триггеров
// старшее слово - идентификатор последнего события
extern void cdecl ResetCallbackCounter (void);
// сбрасывает счетчик триггеров в ноль
extern void cdecl ResumePlaying (void) ;
// продолжает воспроизведение прерванной последовательности
#define SEQ_STOPPED 0 // возвращаемые значения
#define SEQ_PLAYING 1 // функции SequenceStatus()
#define SEQ_DONE 2
extern short cdecl SequenceStatus (void) ;
// возвращает состояние текущей последовательности
extern short cdecl RelativeVolume (short vol);
// возвращает текущую громкость
extern void cdecl SetRelativeVolume (short vol, short time);
// устанавливает громкость на заданный период времени
#define NOBUFFER 1 // нет резидентного буфера
#define FILENOTFOUND 2 // файл не найден
#define FILETOBIG 3 // файл превышает размер
// зарезервированного буфера
#define REGISTRATIONERROR 4 // ошибка регистрации файла XMI
extern short cdecl RegisterXmidiFile (char *fname);
// регистрирует файл по имени
extern void cdecl PollMidPak (void);
// запрос MIDPAK на исполнение музыки
extern long int cdecl MidPakClock (void);
// возвращает значение внутреннего счетчика MIDPAK
extern long int * cdecl MidPakClockAddress (void);
// возвращает адрес таймера MIDPAK
extern short * cdecl TriggerCountAddress (void) ;
// возвращает адрес счетчика триггеров
extern short * cdecl EventIDAddress (void);
// возвращает адрес идентификатора события
extern short cdecl ReportSequenceNumber (void) ;
extern short cdecl InitMP (char *midpak, char *adv, char *ad) ;
// инициализирует драйвер MIDPAK
extern void cdecl DeInitMP (char *midpak);
// выгружает драйвер MIDPAK
Аппаратное обеспечение UART
Разобравшись с программным обеспечением UART, давайте взглянем на его аппаратную поддержку. Нас интересуют только две вещи: куда воткнуть кабель и как сделать разъем.
ПК могут иметь два типа последовательных портов:
§
9-штырьковый (разъем типа DB-9);
§ 25-штырьковый (разъем типа DB-25).
В таблице 14.2 приведена их распайка.
Таблица 14.2. Распайка для последовательных портов ПК.
Провод Функция Обозначение
9-штырьковый разъем
1 Сигнал наличия несущей CD
2 Прием данных RXD
3 Передача данных TXD
4 Сигнал готовности ввода данных BTR
5 Земля GND
6 Сигнал готовности набора данных DSR
7 Запрос на пересылку RTS
8 Сигнал очистки для пересылки CTS
9 Индикатор звонка RI
25-Штырьковый разъем
2 Передача данных TXD
3 Прием данных RXD
4 Запрос на пересылку RTS
5 Сигнал очистки для пересылки CTS
6 Сигнал готовности набора данных DSR
7 Земля GND
8 Сигнал наличия несущей CD
20 Сигнал готовности ввода данных DTR
22 Индикатор звонка RI
Автономные функции
В компьютерной игре каждую секунду происходят сотни (если не тысячи) самых разнообразных вещей и программа должна следить за множеством различных переменных и состояний. Однако большинство действий, которые происходят во время игры, не нуждаются в жестком контроле со стороны основной программы, поэтому было бы неплохо сделать некоторые функции «самодостаочными» с точки зрения инициализации, синхронизации и тому подобных действий. Я назвал такие функции автономными. Я придумал для них такое название с тем, чтобы подчеркнуть присущую им способность совершать операции, не требуя внимания со стороны программы. Программа в течение цикла игры должна только вызывать эти функции в нужный момент, а все остальное они выполнят сами.
Допустим, например, что мы хотим сделать функцию, которая медленно изменяет содержимое регистра определенного цвета. Этот регистр может использоваться для изображения различных мелких деталей на экране. При выполнении функции изменяется цвет регистра, и таким образом изменяется один из цветов экрана. Результат работы этой функции отражается только на изображении. Функция, решающая подобную задачу идеально приспособлена для автономной работы. В Листинге 12.3 приведен текст программы, реализующей такую функцию.
Листинг 12.3. Автономное управление светом.
void Strobe_Lights (void)
{
static clock=0, // Функция имеет собственные часы, поэтому она
// может инициализироваться при первом вызове
first_time=1
//Проверка на первый вызов
if (first_time)
{
first_time=0; // Сброс флага первого вызова
// Инициализация
} // конец оператора if
else // не первый вызов
{
// пора ли выполнять действия?
if (++clock==100)
change color register
//сброс времени
clock=0
} // конец if
} // конец else
} // конец Strobe_Lights
Примечательно то, что функция Store_Lights() самодостаточна. Вызывающая ее функция не должна передавать ей никаких конкретных параметров для того, чтобы она могла инициализироваться и нормально работать. Более того, у этой функции есть свои локальные статические переменные, которые сохраняют свои значения даже после того как функция отработает.
Вот в чем суть автономных функций. Программе требуется только вызывать эту функцию в игровом цикле и она может быть уверена, что цвета будут изменяться. При этом программа избавлена от необходимости отслеживать значения каких-либо переменных, инициализировать функцию и так далее.
Все это, конечно, здорово. Однако есть и одна проблема: эта функция аппаратнозависимая. Как вы уже заметили, функция считает до 100 и затей делает то, что надо, но при этом скорость ее счета зависит от быстродействия компьютера. Чем мощнее компьютер, тем быстрее выполняются вычисления. В результате на одних компьютерах цвета будут изменяться быстрее, чем на других. Чтобы избавиться от такого рода проблем, мы должны уметь каким-то образом узнавать время.
Информация о текущем времени очень важна и она может пригодиться нам не только для того, чтобы что-то делать в определенное время, но и для других самых разнообразных целей. Так, например, нам может понадобиться птичка, пролетающая по экрану каждые пять секунд, или обновление изображения не реже тридцати раз в секунду. Чтобы добиться этого, необходимо иметь под рукой какой-то таймер и способ доступа к нему.
Итак, поговорим о функциях ответа, которые можно использовать как для решения данной задачи, так и для многих других целей.
Битовое отсечение
Битовое отсечение означает вырезание растрового изображения краями экрана или другими границами. Мы не будет обсуждать здесь общие проблемы отсечения. Нас интересует только прямоугольный или оконный тип отсечения. Даже в играх наподобие Wolfenstein или DOOM используется только прямоугольное отсечение границами экрана или прямоугольниками, находящимися внутри экранной области. На рисунке 7.3 показан пример отсечения растрового изображения.
Каким же образом осуществляется отсечение прямоугольного изображения? И, прежде всего, нужно ли оно нам вообще? На этот вопрос можно ответить и «да» и «нет». Если во время игры изображения ваших объектов и персонажей никогда не выходят за границы экрана, то применять отсечение не обязательно. В этом случае необходимо только выполнять логический контроль за тем, чтобы при движении или трансформации объектов образ никогда не выходил за пределы экрана.
Однако, это не всегда допустимо. Например, в трехмерном DOOM'e монстры часто видны на игровом экране только частично. Скажем, видна только правая половина его тела. Это значит, что его левая часть должна быть отброшена при выводе образа и следует применить какой-либо алгоритм битового отсечения.
Мы всегда должны отсекать растровое изображение в тех случаях, когда оно заезжает за пределы экрана (или за пределы других установленных нами границ). Например, в трехмерной игре Wolfenstein (равно как и в DOOM) играющий может менять размер окна с изображением, но картинка никогда не будет выходить за пределы этого окна, так как она отсекается его краями.
Как же мы можем произвести отсечение изображения? Существует два пути:
§ Мы можем проверять каждый отдельный пиксель, находится ли он внутри отображаемой области. Такое отсечение называется отсечением пространства образа. Другими словами, мы обрабатываем каждый из пикселей внутри всего образа и принимаем решение, рисовать его или нет. Эта техника не принимает во внимание геометрические свойства объекта.
Она очень медлительна. (И я подозреваю, что именно такой метод применила фирма Microsoft в своей графике, потому что я никогда не видел блиттинг, работающий более медленно, чем в Microsoft's Graphics Library!) Посему данным методом мы пользоваться не будем;
§ Мы можем учитывать такие геометрические свойства объекта, как его размер, его форма и его положение по отношению к области отсечения. Этот способ называется отсечением области объекта.
Мы воспользуемся вторым приемом. Он довольно прост, по крайней мере, в том виде, в котором я собираюсь показать его на примере. Мы, как всегда, хотим до&тичь максимума производительности и эффективности. А так как отсечение замедляет блиттинг, нужно постараться свести потери времени к минимуму.
Во-первых, мы должны провести предварительный тест и выяснить: находится ли объект внутри ограниченной области? Например, пусть наш объект 16х16 и он находится в точке (1000,1000). Экран ограничен. Поскольку экран имеет размеры 320х200, совершенно ясно, что этот образ окажется невидим, и никакое отсечение здесь не поможет. Таким образом, мы должны проверять каждый объект на предмет того, будет ли он виден хотя бы частично. Алгоритм 7.1 выполняет данный тест.
Алгоритм 7.1. Тест видимости объекта.
// Пусть объект размером Width x
Height
находится в точке (х,у).
//
Размер экрана - Screen Width x Screen Height.
// Для каждого объекта производим следующую операцию:
if (X+Width>0 and X<Screen_Width and Y+Height>0 and Y<Screen_Height)
then
полностью или частично видим
goto отсечение
else
нет, не видим
goto следующий объект
Алгоритм 7.1 не особенно эффективен. Однако он выполняет свою функцию: если объект полностью или хотя бы частично должен появиться на экране, то нужно выполнить отсечение, после чего можно вывести изображение на экран. В противном случае проверяется следующий объект.
После того как мы приняли решение использовать отсечение, мы просто подсчитываем новые размеры и положение границ вырезанного изображения.
Если вы попробуете это себе представить, то легко увидите, что в большинстве случаев отсечение может проводиться не по всем четырем, а только по двум смежным сторонам рисунка, например, по верхней и правой. А в некоторых случаях достаточно отсекать образ вообще только по одной стороне. Это выполняется для всех растровых изображений, имеющих размер меньше целого экрана.
Если же растровое изображение больше экрана хотя бы по одному измерению, то идея «вылетает в трубу». Так обычно бывает, когда мы масштабируем спрайт, имитируя приближение игрока к объекту. Но пока мы будем предполагать, что спрайты всегда меньше игрового поля экрана.
Если вы изучали функцию Draw_sprite() пятой главы или другие ее модификации, то, должно быть, заметили, что в ней выполняется вложенный цикл FOR, в котором битовый образ объекта рисуется по столбцам. Каждый столбец начинается с Х-коордипаты спрайта и спускается вниз по координате Y. Таким образом, мы должны изменить механизм этого цикла так, чтобы принять во внимание новые отправные и конечные координаты (х,у) и изменить исходное местоположение спрайта, если одна из координат выходит за пределы экрана. Не забывайте о том, что если координата Х или Y (либо обе) выходят за пределы экрана, это еще не значит, что ваш объект вообще невидим. На рисунке 7.4 изображен иллюстрирующий это пример.
Давайте изменим функцию Draw_sprite(), чтобы отсечь данные, не попадающие на экран. Новая версия будет работать на 5 процентов медленнее, однако она всегда будет корректно рисовать спрайты. Листинг 7-3 содержит ее исходный текст.
Листинг 7.3. Новая функция Draw_Sprite() с отсечением.
void Draw_Sprite_Clip(sprite_ptr sprite)
{
// Эта функция рисует спрайт на экране и отсекает области,
// выходящий за границы экрана. Функция написана в
// демонстрационных целях.
char far *work_sprite;
int work_offset=0,offset,x_off,x,y,xs,ys,xe,ye, clip_width, clip_heigth;
unsigned char data;
// Получить указатель на спрайт
xs=sprite->x;
ys=sprite->y;
// Расчет координат прямоугольника, описывающего спрайт
xe=xs+sprite_width-1;
ye=ys+sprite_height-1;
// Проверка полной невидимости спрайта
// ( то есть лежит ли он полностью за пределами экрана)
if((xs>=SCREEN_WIDTH) || (ys>==SCREEN HEIGHT) | |
(xs<=(-sprite_width))||(ys<=(-sprite_height)))
{
return;
} // конец оператора if
// Спрайт частично видим, следовательно, необходимо
// рассчитать рисуемую область
// Отсечение по координате Х
if(xs<0)
xs=0;
else
if(xe>=SCREEN_WIDTH) xe=SCREEN_WIDTH-l;
//Отсечение по координате Y
if(ys<0)
уs=0;
else
if(ye>=SCREEN_HEIGHT) ye=SCREEN_HEIGHT-1;
// Расчет новой высоты и ширины
clip_width=xe-xs+l;
clip height=ye-ys+l;
// Расчет рабочего смещения на основе нового начального
// значения
координаты
Y
work_offset=(sprite->y-ys)*sprite_width;
x_off=(xs-sprite->x);
// Построение усеченного спрайта
// Для упрощения дадим указателю на спрайт новое имя
work_sprite = sprite->frames[sprite->curr_frame];
// Расчет смещения,спрайта в видеобуфере
offset = (ys<< 8) + (ys<< 6) + sprite->xs;
for (y=0; y<clip_height; y++)
{
// Копируем следующую строку в видеобуфер
for (х=0; x<clip_width; x++)
(
// Проверка пикселя на "прозрачность" (то есть на 0);
// если пиксель "непрозрачный" - рисуем
if ((data=work_sprite[work_offset+x+x_off]))
double_buffer[offset+x+x_off] = data;
} // конец цикла по X
// Переход к следующей строке в видеобуфере
// и в растровом буфере спрайта
offset += SCREEN_WIDTH;
work_offset += sprite_width;
} // конец цикла по У
} // конец функции
Как и к двум первым листингам этой главы, к Листингу 7.3 надо относиться достаточно скептически. Эта программа, конечно, делает то, что ей и положено, однако не учитывает при этом контекста. А контекст — это все! Например, в вашей игре все картинки могут располагаться посередине. Тогда в отсечении у вас просто отпадет необходимость. Возможно, ваш объект всегда будет двигаться по горизонтали, в этом случае не потребуется выполнять вертикальное отсечение и вы сможете провести оптимизацию части предварительной обработки.Иными словами, всегда пытайтесь создавать ваши функции для конкретной ситуации и для определенной игры. В данном случае передо мной стояла несколько иная задача — объяснить вам суть проблемы. Если вы поняли идею, то, потратив определенное время и силы, найдете свое правильное решение.
Частота
4DAEh
19886
60Hz
965C
39722
30Hz
E90B
59659
20Hz
Таким образом, нам следует перепрограммировать счетчик 0 таким образом, чтобы он генерировал прерывания с более подходящей для нашей игры частотой - так, чтобы наша игра могла контролировать события чаще чем 18.2 раза в секунду.
Давайте посмотрим, каким образом можно задать соответствующее значение счетчика. Нас интересуют порты ввода/вывода 40h и 43h (все, о чем мы будем говорить, относится к ним): счетчик 0 и соответствующий управляющий регистр. Значения битов управляющего регистра, показаны в таблице 12.4.
Из таблицы 12.4 видно, что в нашем распоряжении много параметров. Впрочем, нам сейчас все они не нужны, нам надо всего-навсего изменить начальное значение счетчика 0. При этом значения других битов должны быть следующими:
§
Режим работы - 2, генератор частоты;
§ Метод подсчета - двоичный;
§ Запись регистра счетчика осуществляется при помощи операции «Прочитать/записать младший байт, а затем старший байт счетчика».
Таким образом, все что мы должны сделать, это записать управляющее слово в порт 43h, а затем выполнить две операции записи в порт 40h. В ходе первой операции записи мы установим значение младшего байта нового значения счетчика, а при второй — значение старшего байта. В том, где какие байты аппаратные средства разберутся сами, поэтому вам об этом волноваться не надо. В Листинге 12.5 приведен текст функции, программирующей таймер.
Листинг 12.5. Перепрограммируем системные часы (OUTATIME.C).
// ВКЛЮЧАЕМЫЕ ФАЙЛЫ /////////////////////////////////////
#include<stdio.h>
#include<conio.h>
// ОПРЕДЕЛЕНИЯ //////////////////////////////////////////
#define CONTROL_8253 0х43 // управляющий регистр 6253
#define CONTROL_WORD
0хЗС // управляющее слово, задающее режим 2,
// двоичный подсчет, запись
// младший/старший байт
#define COUNTERED 0х40 // счетчик
0
#define TIMER_60HZ 0x4DAE // 60Гц
#define TIMER_30HZ 0x965C // 30Гц
#define TIMER_20HZ 0xE90B // 20Гц
#define TIMER_18HZ 0xFFFF // 18.2Гц (стандартная
частота)
// МАКРОСЫ //////////////////////////////////////////////
#define LOW_BYTE(n) (n & 0x00ff)
#define HI_BYTE(n) ((n>>8) & 0x00ff}
// ФУНКЦИИ //////////////////////////////////////////////
void Change Time(unsigned int new count)
{
// послать управляющее слово, задающее режим 2, двоичный подсчет,
// запись
младший/старший'байт
_outp(CONTROL_8253, CONTROL_WORD);
// теперь
запишем младший
значащий байт
в регистр
счетчика
_outp(COUNTER_0,LOW_BYTE(new_count));
//я теперь запишем старший байт в регистр счетчика
_outp(COUNTER_0,HI_BYTE(new_count) ) ;
} // конец Change_Time
// ОСНОВНАЯ ПРОГРАММА ///////////////////////////////////
main()
{
// перепрограммирование таймера с частоты 18.2Гц на частоту 60Гц
Change_Time (TIMER_60HZ);
} // конец,функции main
После окончания этой программы системный таймер будет работать слишком быстро для внутренних часов DOS. Для того чтобы это поправить, вы можете:
§ Перезагрузить компьютер;
§ Запустить программу еще раз, изменив константу, передаваемую функции Change_Time (), на TIMER_18HZ.
Частотный синтезатор
Важнейшей частью звуковой карты Sound Blaster является частотный синтезатор. Как я уже говорил в этой главе, чазтотный синтезатор создает нужный сигнал путем модулирования несущей волны. Это позволяет получать гармоники. Более того, с помощью этого метода можно аппроксимировать частотные характеристики звуков реальных музыкальных инструментов и настоящего человеческого голоса.
Частотный синтезатор имеет 18 управляющих блоков, каждый из которых состоит из двух частей:
§ Блок модулятора;
§ Блок несущей.
Рисунок 9.8 показывает блок типичного частотного синтезатора. Здесь А -общая амплитуда, Wc - частота несущей волны (радиан/с), Wm - частота модулятора (радиан/с).
Выходной сигнал блока модулятора добавляется к сигналу блока несущей волны. В результате этого одна волна «оседлывает» другую. Математически это описывается произведением двух синусоидальных волновых функций. Выходная функция имеет следующий вид:
F(t) = А
* sin(Wc * t + I * sin(Wm) * t)
К сожалению, у нас нет времени надолго задерживаться на разговоре об устройстве синтезатора. Нам надо писать игру! (Хотя, конечно, нам и очень нужна потрясающая музыка!)
Чтение позиции джойстика
Чтение позиции джойстика — весьма утомительная, но вполне выполнимая задача. Все, что нам надо сделать, это послать джойстику простую команду. Это делается записью значения 0 в порт 201h. Затем мы ждем, когда установится нужный нам бит (0-3) порта джойстика. Во время ожидания мы должны включить счетчик. Когда нужный бит установлен, то число, которое мы насчитаем, и есть позиция джойстика. Листинг 3.2 показывает код, который все это делает.
Листинг 3.2. Чтение позиции джойстика.
unsigned int Joystick{unsigned char stick)
{
asm {
cli ;запретить прерывания
mov ah,byte ptr stick ;замаскировать АН,
;чтобы выбрать джойстик
хоr аl,аl ;обнулить AL
xor cx,cx ;обнулить СХ
mov dx,JOYPORT ;используем DX для ввода и вывода
out dx,al
discharge:
in al,dx ;читаем данные из порта
test al,ah ;изменился ли бит запроса?
loopne discharge ;если нет, повторяем чтение
sti ;разрешить прерывания
хог ах, ах ;обнулить АХ
sub ах,сх, ;теперь АХ содержит позицию джойстика
} // конец ассемблерного блока
// возвращаемое значение содержится в АХ
} // конец функции
(Кстати, встроенный ассемблер мне все больше и больше нравится.) Программа достаточно проста: при запуске программа обнуляет регистры АХ и СХ;
§
Затем программа опрашивает порт джойстика;
§ Далее подсчитывается количество циклов в ожидании, пока установится нужный бит;
§ Подсчет выполняется в регистре СХ с помощью инструкции LOOPXX (в данном случае используется команда LOOPNE);
§ Инструкция TEST определяет установку бита;
§ Когда нужный бит установлен, программа выходит из цикла. Результат передается вызывающей программе, в регистре АХ.
С этим вроде все. Позже я покажу демонстрационную программу, в которой применяются все функции джойстика. Благодаря обращению к BIOS она обладает лучшей совместимостью, легче переносима и умеет производить самокалибровку. Я думаю, что и вы будете пользоваться ею.
Чтение символа из буфера
Теперь нам необходимо иметь возможность считывать символ из буфера. Это легко. В принципе, достаточно запомнить новый индекс, указывающий на текущую ячейку буфера, из которой будет прочитан следующий символ. Но что если мы попытаемся прочитать символ и изменить индекс, в то время как основная программа уже исчерпала все входные данные, пришедшие от прерывания? В этом случае функция просто-напросто будет возвращать символ 0. Листинг 14.2 содержит подходящую программу.
Листинг 14.2. Функция Serial Read.
int Serial_Read()
{
// функция возвращает последний записанный
//в программный буфер символ
int ch;
//ждем завершения функции обработки прерывания
while(serial_lock){}
//проверяем, есть ли символы в буфере
if (ser_end != ser_start)
{
// меняем значение начальной позиции буфера
if (++ser_start > SERIAL_BUFF_SIZE-1) ser_start = 0;
// читаем символ
ch = ser_buffer[ser_start];
// в буфере стало одним символом меньше
if (char_ready > 0) --char_ready;
// возвращаем символ'вызвавшей функции
return(ch) ;
} // конец действий, если буфер не пуст
else
// буфер был пуст - возвращаем 0
return(0);
} // конец функции
Функция serial_read получает следующий доступный символ из буфера и возвращает его. Если в буфере не осталось данных, она возвращает 0.
Что детализировать, а что нет?
Возникающие в игре ситуации заставляют использовать при разработке палитры определенные сочетания цветов. Если у вас есть несколько фотографий или трехмерных изображений, которые вы собираетесь использовать, желательно показать их в игре максимально приближенными к оригиналу. Позже я расскажу, как можно оптимизировать палитру, объединяющую столько различных оттенков, сколько требуется для придания изображению максимально правдоподобного вида.
Что такое многозадачность?
Многозадачность — это не что иное, как одновременное выполнение нескольких процессов на одном и том же компьютере. Например, Microsoft Windows представляет собой многозадачную операционную систему (некоторые, возможно, возразят мне, что она еще нуждается в некоторых дополнениях для того, чтобы ее можно было назвать настоящей многозадачной системой, однако для нашего случая подобный пример вполне подойдет). При желании в Windows пользователь вслед за одной программой может запустить и другую. Компьютер при обработке данных разбивает каждую секунду на несколько «тиков». Каждый процесс или программа получает в свое распоряжение несколько таких «тиков» работы процессора. По истечении отведенного на эту программу времени процессор переходит к обработке следующей программы и так далее. Так продолжается до тех пор, пока не будут обслужены все выполняющиеся программы, после чего процесс повторяется. Компьютер работает настолько быстро, что пользователю кажется, будто все программы работают одновременно. На рисунке 12.1 показано, как реализуется многозадачность.
При создании компьютерной игры нам незачем заботиться об обеспечении совместной работы нашей игры и, например, текстового процессора. Нас должна Интересовать организация совместной работы различных частей одной программы. Это означает, что нам нужно сделать так, чтобы каждая функциональная часть нашей игры обрабатывалась в течение некоторого времени, причем не слишком долго. (Под функциональной частью в данном случае я понимаю Фрагменты текста программы (подпрограммы), которые осуществляют вывод графического изображения, воспроизведение звука, а также реализуют логику самой игры). После того как каждая подпрограмма по очереди будет обработана, процесс должен повторится с начала, образуя игровой цикл.
Мы можем считать, что отдельная программа на Си, представляющая собой процедуру, вызывающую функциональные части, является своего рода маленьким виртуальным компьютером. При этом каждая из упомянутых функций это не что иное, как отдельная задача, выполняемая в псевдомногозадачном режиме.
Отметим, что программа всегда контролирует выполнение функциональных частей, и игра может быть написана таким образом, что исполнение программы и функциональных частей никогда не прерывается другими процессами:
На самом деле это очень удобно: сложные компьютерные игры могут быть написаны и пишутся в виде единой программы безо всяких прерываний. Однако прерывания позволяют реализовать хоть и примитивную, но истинную многозадачность следующим образом;
§
Выполнение программы может прерываться в любой произвольной точке. Состояние машины сохраняется таким образом, чтобы впоследствии можно было возобновить работу программы;
§ После этого управление персональным компьютером передается обработчику прерывания, который представляет собой отдельную законченную программу;
§ После завершения работы обработчика прерывания или процедуры обслуживания прерывания (ISR) возобновляется выполнение программы с того момента, с которого она была прервана.
На рисунке 12.2 показано, как происходит обслуживание прерывания.
Вы можете сказать: «Но зачем нам это нужно?» Поверьте, есть много причин использовать прерывания. Ну, например, допустим, что некоторые данные вводятся в вашу программу с клавиатуры. Возможен вариант, когда пользователь нажмет на клавишу в момент вывода па экран графического изображения. Тогда нажатие клавиши будет проигнорировано. Однако если при нажатии клавиши вызывается процедура обслуживания прерывания, то процессор переключит свою работу на обработку нажатия па клавишу. Таким образом можно быть уверенным, что информация о нажатии на клавишу никогда не будет потеряна.
В случае компьютерных игр нас интересует не столько ввод данных с клавиатуры, сколько синхронизация. Синхронизация для игры - это все. Изображение должно появляться и изменяться в нужный момент. Музыка должна звучать в подходящем темпе. Иногда мы хотим, чтобы то или иное событие произошло через определенный промежуток времени, скажем через 1/30 или 1/60 долю секунды.
Без прерываний реализовать это на персональном компьютере сложно, так как понадобится отслеживать время» а это лишняя нагрузка на процессор.
Другая часто возникающая проблема заключается в том, что одна и та же подпрограмма на разных компьютерах будет исполняться с различной скоростью. Это означает, что расчет времени в игре является аппаратнозависимым.
Благодаря использованию прерываний и многозадачности мы решаем и еще одну проблему — освобождаем программу от опроса всех периферийных устройств. Когда игровой цикл не только реализует логику игры, но и должен непрерывно опрашивать состояние последовательного порта, выяснять, не переместилась ли рукоятка джойстика, не нажата ли клавиша на клавиатуре, не кончилось ли воспроизведение музыки — это просто много лишней головной боли.
Мы можем попробовать выделить задачи, которые непосредственно не связаны с выполнением основной программы и реализовать их в виде прерывании. Только не поймите меня превратно. Персональный компьютер не сможет предоставить нам в распоряжение тысячи прерываний на все случаи жизни. На самом деле мы сможем использовать лишь пару прерываний и только в определенных целях.
Если вы уже окончательно запутались, то... для чего же еще нужны друзья как не для того, чтобы помогать? Давайте попробуем вместе разобраться с этой путаной реализацией многозадачности в DOS.
Что такое «расширенный MIDI-формат»?
Так как пакет программ MIDPAK использует драйверы AIL Miles Design, то он не проигрывает MIDI-файлы напрямую. Ваш MIDI-файл должен быть преобразован в расширенный MIDI-формат. Этот формат был разработан Miles Design. Он поддерживает несколько MIDI-последовательностей в одном файле, причем с помощью команды PlaySequence пакет программ MIDPAK позволяет переходить от одной последовательности к другой практически мгновенно. Процесс добавления нескольких MIDI-файлов в один расширенный MIDI-файл (XMI) выглядит следующим образом. Допустим, у вас есть три музыкальных файла SONGA.MID, SONGB.MID и SONGC.MID, и вы хотите поместить их в один файл XMI. Для этого в командной строке DOS надо выполнить следующую команду:
MIDIFORM SOMG.XMI SONGA.MID SONGB.MID SONGC.MID
Команда поместит эти три MIDI-последовательности в один файл расширенного MIDI-формата SONG.XMI. Вы можете манипулировать ими из MIDPAK, используя функции PlaySequence(0), PlaySequence(l) и PlaySequence(2).
Что такое трехмерное пространство
Трехмерное пространство... Звучит как строка из фантастического рассказа... Ну очень похоже. Трехмерное пространство - это просто расширение двухмерной плоскости. Надо сразу отметить, что рендеринг трехмерной графики весьма сложен. Вообще же, сложность рендеринга экспоненциально возрастает с добавлением новых измерений и происходит это оттого, что и сами образы при этом усложняются. Наша задача заключается в том, чтобы понять, как работать с новым измерением. Мы должны все это изучить.
Говоря языком математики, любая точка в трехмерном пространстве описывается с помощью уникального набора трех координат: х, у и z. Как мы уже обсуждали ранее, обычно экран представляется плоскостью Х и Y, а координата z перпендикулярна экрану.
В отличие от плоскости, где х-координата горизонтальна, а у-координата вертикальна, трехмерные системы координат бывают двух типов:
§ Левосторонняя система координат;
§ Правосторонняя система координат.
На рисунке 6.1 показано представление обоих способов отображения трехмерных систем.
В дальнейшем для всех наших рассуждений, примеров и программ мы будем использовать только правостороннюю' систему координат. На то есть две причины:
§ Правосторонняя система удобней в работе, поскольку в ней проще выполняется визуализация;
§ Правосторонняя система распространена как стандарт.
Конечно, вы можете сказать, что экран компьютера — это плоскость, и мы не можем преобразовывать трехмерные образы в двух измерениях. Верно. Вы абсолютно правы. Но у нас есть возможность отобразить их на плоскость. Мы даже можем видеть «тени» объектов. И сделать это позволит проекция. При этом модель выглядит на двухмерном экране так, что у зрителей возникает полное ощущение объемности. Такие игры как DOOM и Wolfenstein трехмерны только в нашем восприятии. Можно сказать, что в них смоделирован особый случай трехмерного пространства, образы которого можно обрабатывать гораздо проще и быстрее.
В любом случае, мы еще к этому вернемся, а теперь давайте поговорим об основных понятиях трехмерного пространства.
Что вы должны знать
Для понимания большинства материала, представленного в этой книге, вы должны хорошо владеть языком программирования Си. Большинство книг по созданию компьютерных игр ориентируются на ассемблер. В этой книге ассемблеру уделено совсем немного внимания, а основной акцент сделан на Си. Однако, она содержит несколько ассемблерных примеров, так что знакомство с этим языком вам не помешает.
Что вы узнаете из этой книги?
Эта книга написана, чтобы научить читателя создавать трехмерные видеоигры типа DOOM или Wolfenstein 3-D. Эту книгу не стоит рассматривать как учебник по видеографике. Книга написана так, что в ней освещаются вопросы, связанные именно с играми и разработкой игр.
Для того чтобы научиться писать игры, нам придется очень серьезно потрудиться, но я уверен, что когда вы закончите работу над этой книгой, то сможете самостоятельно разработать серьезную игру. Чтобы совсем быть в этом уверенным, мы с вами начнем писать игру. Эта такая трехмерная игра типа Wolfenstein 3-D. Впрочем, подойдя к последней главе, вы сами ознакомитесь с тем, что у меня получилось.
Чудовища и прочие кровожадные персонажи
Стоит ли говорить, что, рисуя чудовищ для игры, можно отбросить в сторону всякие правила и забыть о соблюдении пропорций. Некоторые из ваших монстров, скажем, могут иметь головы в половину всего тела! Единственный принцип, которым нужно руководствоваться при их создании - это богатое воображение. Такое занятие больше похоже на развлечение: вам не нужно беспокоиться на счет реализма, ибо никто в действительности не знает, как на самом деле должен выглядеть предполагаемый монстр!
Вокруг имеется обилие источников, которые могут подогреть вашу фантазию. В ближайшей библиотеке наверняка найдется фантастическая литература с иллюстрациями и в видеопрокате можно подобрать подходящий фильм. Комиксы также могут послужить хорошим источником идей. Изображение чудовищ и демонов в искусстве прошло длинный путь, так что без особого труда можно подыскать на эту тему работы художников многих столетий. Но помните: делайте ваших монстров как можно более оригинальными, чтобы ненароком не нарушить авторских прав.
Файл ЕХАМР16.РСХ показывает несколько кадров с одним и тем же монстром. Как видите, он выглядит совершенно по-разному, если изменять его цвет. Это говорит о том, что даже небольшие изменения могут в корне преобразить ваше создание! Возможно, в дальнейшем вы составите целую библиотеку изображений всевозможных чудовищ.
Цветовая палитра
При создании оцифрованного изображения программное обеспечение пытается определить оптимальный набор цветов для каждого из сканируемых кадров в отдельности. Это не подходит для нашего случая, так как у нас должна быть одна и та же палитра для всех кадров. При желании мы, конечно, могли бы пользоваться несколькими палитрами, но тогда пришлось бы менять их каждый раз при смене ракурса объекта. А это представляет определенную сложность.
Я обнаружил, что можно записать цветовую палитру и заставить Video for Windows использовать ее для всех кадров. Это делается следующим образом:
§ Используйте функцию Capture Palette для захвата 20-30 кадров с вашей моделью. Будьте аккуратны, объект не должен сдвигаться!
§ Используя эти 20-30 кадров, программа усреднит палитры каждого изображения и подберет оптимальную. Затем вы должны будете использовать эту палитру для всей последовательности кадров.
Все это замечательно, но в данном случае окажутся задействованными все 256 цветов палитры. В результате все прочие изображения в вашей игре будут
вынуждены использовать эту сгенерированную Video for Windows палитру, а вовсе не ту, которая вам нравится. Как же этого избежать?
Что касается меня, то я предпочел бы, чтобы захват кадра, происходил только с использованием 64 цветов. Тогда я смог бы оставшиеся 192 цвета использовать для теней, смешивания тонов, освещения и других вещей. Я не знаю, как это сделать с помощью упомянутого оборудования и программ. Единственная надежда на то, что ваше программное обеспечение позволит получить произвольно заданную цветовую палитру. Я никогда ничего подобного не видел, но уверен, что такое существует.
Наконец, вполне реально для решения этой проблемы написать собственную программу. Статистически обработав все изображения, не сложно определить наиболее часто использующиеся цвета. Затем можно поступить следующим образом:
§ Создать палитру из 64 (или более) наиболее часто встречающихся цветой;
§ Перерисовать изображения пиксель за пикселем с помощью новой палитры, используя из имеющихся оттенков наиболее близкие цвета.
Таким образом, вы сможете соединить палитру игры и палитру оцифрованных изображений. На самом деле это именно тот метод, который я использую сам, поэтому я точно знаю, что он работает.
Цветовая ротация
Когда я купил свой первый компьютер в 1978 году (хотя я до сих пор уверен, что это был божий дар) я был глубоко поражен компьютерной графикой и видеоиграми. Компьютер назывался Atari 800. В то время это было пределом возможностей. Один из интересных эффектов, который он поддерживал, назывался цветовой ротацией. Цветовая ротация может быть выполнена только на компьютерах, имеющих таблицу преобразования цветов. Как известно, изображение рисуется на экране компьютера. Например, пусть это будет водопад. Изображение водопада состоит (в нашем случае) из 16 оттенков синего цвета, каждый из которых — это число, ссылающееся в таблице преобразования на соответствующее значение цвета. Мы получаем, что водопад содержит 16 оттенков синего, находящихся в регистрах цвета с адресами от 100 до 115.
Теперь представьте, что мы берем одно из значений и сдвигаем его в следующий регистр и так далее до 115-го, содержимое которого переносим в 100-й регистр. Что произойдет? Правильно, возникает ощущение движения. Уже в 70-х годах это было возможно на процессорах 6502 с тактовой частотой 1.79МГц.
Мы применим эту технику позже. Есть куча классных вещей, которые можно сделать, используя этот прием, тем более, что в ПК сейчас целых 256 регистров. А пока постарайтесь просто запомнить этот трюк.
Демонстрационная программа Test.c
Ниже приводится демонстрационная программа на языке Си, которая показывает, как загружать и работать с драйверами DIGPAK и MIDPAK.
/* */
/* TEST.C Демонстрационная программа работы с драйверами DIGPAK */
/* и MIDPAK. Динамически загружает драйверы DIGPAK
и MIDPAK, */
/* SOUNDRV.COM и MIDPAK.COM/MIDPAK.ADV/MIDPAK.AD. Затем */
/* воспроизводит MIDI-файл TEST.XMI, позволяя вам исполнять */
/* звуковые эффекты TEST1.SND и TEST2.SND */ /****************************************************************/
/* Автор: John W. Ratcliff (с) 1994 */
/* CompuServe: 70253,3237 */
/* Genie: J.RATCLIFF3 */
/* BBS: 1-314-939-0200 */
/* Адрес: */
/* 747 Napa Lane */
/* St. Charles, МО 63304 */
/* */
/* */
/*********************************************'******************/
#include <stdio.h>
#include <stdlib.h>
#include <malloc.h>
#include "keys.h" // Включает определения для
// клавиатурных команд
#include "support.h" // Включает файл заголовка базовых
// функций поддержки
#include "loader.h" // Включает файл заголовка динамического
// загрузчика MIDPAK/DIGPAK
#include "midpak.h" // Включает файл заголовка функций
// нижнего уровня MIDPAK
#include "digplay.h" // Включает файл заголовка функций
// нижнего уровня DIGPAK
#include "doscalls.h" // Включает файл заголовка
// DOS-функций поддержки
#define NOBJ 4 // число загружаемых звуковых эффектов
static char *Names [NOBJ]= // имена файлов звуковых эффектов
{
"TEST1.SND", "TEST2.SND", "PEND.3ND", "TEST.SND" };
static SNDSTRUC *snd; // Структура звуковых данных DIGPAK
static char *soundbuffer=0; // Адрес буфера звуковых данных
static long int ssize[NOBJ]; // Длина загружаемого звукового эффекта
static int NALLOC=0;
char *Sounds[NOBJ]; // адреса всех звуковых эффектов
void UnloadSounds(void); // Выгрузка звуковых эффектов из памяти
int LoadSounds(void); // Загрузка звуковых эффектов в память
void PlaySound(int sound); // Воспроизведение звукового эффекта
void TestDigPak(void); // Тестирование функций
// воспроизведения DIGPAK
// Приложение должно обеспечить функции резервирования памяти. Они
// используются загрузчиком и DOS функциями поддержки. Вы можете
// заменить эти функции любой другой системой управления памятью.
unsigned char far * far memalloc(long int siz) {
unsigned char far *mem;
mem = farmalloc(siz) ; // функция Си для резервирования
// памяти по дальнему адресу return(mem);
}
void far memfree(char far *тетогу)
farfree(memory); // функция Си для освобождения памяти по
// дальнему адресу
}
void main(void)
{
long int siz;
char *fname;
// Вызов загрузчика для начальной загруаки стандартного
// драйвера DIGPAK
if ( !LoadDigPak("SOUNDRV.COM") )
{
printf("Failed to load sound driver.\n");
exit(l);
} if ( !InitDigPak() ) // Инициализация драйвера DIGPAK
{
// Выгрузка драйвера из памяти в случае неудачной
// инициализации и завершение работы программы.
UnLoadDigPak();
printf("Failed to initialize sound driver.\n") ;
exit(l);
}
if ( LoadMidPak('MIDPAK.COM", "MIDPAK.ADV", "MIDPAK.AD") ) // Загрузка компонентов MIDPAK {
printf("Loaded MIDPAK.СОМ MIDPAK.ADV and MIDPAK.AD into Low Men\n");
if ( InitMidPak() )
{
printf("MIDPAK driver initialized.\n");
fname = floadlow("TEST.XMI",&siz); // Загрузка музыки
if ( fname }
{
printf("Loaded TEST.XMI %d bytes long.\n",siz);
RegisterXmidi(fname,siz); // Регистрация XMIDI
// последовательности
printf("Sequence registered, now playing.\n") ;
PlaySequence(0); // Исполнить первую
// последовательность
SegueSequence (1,-1); // Зарегистрировать вторую
// последовательность на
// исполнение сразу же по
// достижении первого
// Контроллера 119
}
} else
printf("Failed to initialize MIDPAK driver.\n");
}
TestDigPak(); // Тестирование/демонстрация возможностей функций
// DIGPAK
UnLoadMidPak(); // Выгрузка MIDPAK из памяти,
// освобождение аппаратуры
UnLoadDigPak(); // Выгрузка DIGPAK из памяти,
// освобождение аппаратуры
RemoveVectorLoader() ;
}
void TestDigPak(void)
{
int i,key,sound;
printf("Loading digital sound effects.\n");
if ( LoadSounds() ) // Загрузка звуковых эффектов в память
{
// Создание меню звуковых эффектов
printf(" Select an sound effect to play. [ESC] when finished playing around.\n");
for (i=0; i<NOBJ; i++)
{
printf("%c %s\n",i+'A',Names[i]) ;
}
do
{
if ( keystat() ) // если клавиша нажата
{
key = getkey(); // получить нажатую клавишу
if ( key >= ' a' && key <= ' z') key-=32;
// преобразовать нижний регистр к верхнему
if ( key >= 'А' && key <= 'Z')
{
sound = key-'A';
if ( sound < NOBJ ) PlaySound(sound);
}
} while ( key != 27 );
UnloadSounds(} ;
}
}
// загрузить все звуковые эффекты в память
int LoadSounds(void)
{
int fph;
long int siz.end;
int i, handler;
int select;
for (i=0; i<NOBJ; i++)
{
Sounds[i] = fload(Names[i], &siz);;
if ( !Sounds[i] )
{
printf("File '%s' not found.\n"/Names[i]) ;
return(0) ;
}
ssize[i] = siz;
snd.frequency = 11000;
snci. sound = Sounds [i];
snd.sndlen = ssize[i]; // задает длину звукового эффекта
//в байтах
MassageAudio(&snd) ;
printf("Sound Loaded '%s'.\n",Names[i]);
}
return(1) ;
}
void UnloadSounds(void) {
int i;
for (i=0; i < nalloc; i++) memfree(Sounds[i]);
if ( soundbuffer )
{
realfree(snd) ;
realfree(soundbuffer);
}
NALLOC=0 ;
}
void PlaySound(int sound)
{
if ( !soundbuffer }
{
snd = (SNDSTRUC *) realalloc(sizeof(SNDSTRUC));
snd->frequency = 11000;
soundbuffer = realalloc(65535);
}
StopSound(); // ожидать до окончания предыдущего звукового эффекта
snd.frequency = 11000;
snd.sound = Sounds[i];
snd.sndlen = ssize[i]; // задает длину звукового эффекта
//в байтах
DigPlay2(&snd); // воспроизвести эффект
//в аппаратно-зависимом формате
}
Демонстрационный режим
После того как вы окончите работу над созданием компьютерной игры, вы наверняка захотите включить в нее режим демонстрации для показа ее возможностей. Если вы создавали игру с учетом такой необходимости (например, предусмотрели возможность получения исходных данных из файла, а не от игрока), то сделать это достаточно просто.
Во время игры изменяется состояние устройства ввода-вывода. (Более подробно об этом можно прочесть в третьей главе, «Основы устройств ввода»). С помощью этих устройств игрок может влиять на события. Во время игры логический контроль за вводом осуществляется с помощью определенной функции, такой, например, как Get_Input () . Эта функция отвечает за отслеживание текущего состояния устройства ввода.
Таким образом, все что мы должны сделать, это добавить в функцию контродя за вводом возможность считывания данных из заранее созданного файла. Сама по себе игра не почувствует никакой разницы. Рисунок 11.8 наглядцо показывает реализацию этого метода.
Но, спросите вы, как создать такой файл? Для этого необходим особый режим игры. В таком режиме данные от устройства ввода будут оцифровываться с некоторым постоянным интервалом времени (скажем, 10 раз в секунду) и записываться в специальный файл. В демонстрационном режиме функция ввода будет вместо информации от устройства ввода использовать информацию из этого файла.
Это самый распространенный метод реализации демонстрационных режимов. Конечно, существуют и другие способы, но все они используют либо заранее подготовленные данные определенного формата, либо подобия системы искусственного интеллекта.
Вы можете решить, что сохранение всей информации о вводе потребует много памяти. Давайте посмотрим. Если мы сохраняем состояние устройства ввода 10 раз в секунду, то за минуту набежит 600 байт, а за 10 минут - всего 6 килобайт. Как видите, это не проблема, поэтому я советую вам пользоваться именно таким методом. Однако еще до начала
составления программы вы должны предусмотреть в функции ввода «переключатель», с помощью которого можно выбирать между мышью, клавиатурой, джойстиком или воспроизведением файла. Возможность воспроизведения файла позволяет также реализовать в игре режим "замедленного повтора".
Детали, детали и детали
Одним из моментов, требующих наибольшего внимания при разработке игровой графики, является включение в изображение мелких деталей. Это мастерство приходит с практикой и ему стоит поучиться. Например, голая каменная стена выглядит не слишком убедительно, но если на ней подрисовать трещины, пучки мха и другие незначительные подробности, это заметно улучшит ее внешний вид. Кроме того, такие дополнительные мазки придадут окончательному виду вашей игры большую индивидуальность. Запомните: каждый, даже самый мелкий штрих приоткрывает окно в ваш воображаемый мир, и вы должны заставить поверить игроков, что они действительно в него попали!
Разработку стен, пола и «кирпичиков» пoтoлкa лyчшe проводить в увеличенном масштабе изображения, используя команду ZOOM. Это полезно по двум причинам:
§
При добавлении мелких деталей это помогает точно выбирать те пиксели, которые нужно изменить;
§ Кроме того, вы сразу же видите, как «кирпичики» выглядят вблизи и при необходимости сможете вносить изменения, чтобы улучшить их внешность.
Давайте шаг за шагом рассмотрим этапы создания «кирпичика» стены. Эти шаги изображены в файле ЕХАМР06.РСХ.
1. Начнем с того, что с помощью горизонтальных линий, изменяющих свой цвет от темносерого до почти белого и наоборот, изобразим прямоугольник размером 64х64, как показано на рисунке 16.6. Плавный переход цветов создает иллюзию того, что панель управления изготовлена из нержавеющей стали или алюминия.
2. Затем нарисуем черный
прямоугольник, в котором будет располагаться дисплей панели управления (рис. 16.7).
3. Следующим шагом обведем
черный прямоугольник линией медного цвета (рис. 16.8).
4. Чуть ниже дисплея панели управления расположим несколько линии темного (25% черного) цвета, как вы можете видеть из рисунка 16.9. Это слегка оттеняет серый тон, делая верхнюю часть изображения более привлекательной.
5. На пятом шаге добавим детали панели управления, показанные на рисунке 16.10.
Мы нарисуем красные и желтые яркие пиксели, изображающие светодиоды, яркокрасные цифры и яркосинюю кривую, напоминающую осциллограмму.
6. Теперь, когда верх экрана проработан, нижняя часть стала выглядеть еще более пустой, так что надо ее чем-то заполнить. Для создания ощущения трехмерности добавим несколько серых теней расцветим их вертикальным градиентом. Эти изменения отображены на рисунке 16.11.
7. Затем оттеним отведенную для выключателя область светлыми и темными тонами, чтобы придать ей выпуклый вид (рис. 16.12).
8. Наконец, нарисуем сам выключатель, выполненный в медных тонах. вот мы и получили лицевую сторону панели управления! Окончательный результат показан на рисунке 16.13.
Для сооружения стен изготовим несколько новых "кирпичиков", внеся •некоторые изменения в уже нарисованный. Затем, используя премы вырезания и приклеивания, размножим созданные «кирпичики» и выстроим их в ряд,
чтобы убедиться в том, что соседние области на стыках гармонируют друг с другом. Это позволяет избежать противоречий в соединении графических элементов, появляющихся в игре, а также помогает обойтись без швов при выводе немозаичных участков изображения. Повторю, что шаги, приведенные здесь, продемонстрированы в файле ЕХАМР06.РСХ.
Директива LOCAL
Заметьте, что в Листинге 2.4 мы изменили значение регистра SP не только в начале процедуры, но и в конце (перед тем как восстановить регистр ВР). Эта техника обычно используется для размещения переменных в ассемблерных процедурах при их вызовах из языков высокого уровня.
В листинге 2.4 это делалось вручную. А вот MASM 5.1 и более поздние версии имеют встроенную директиву, которая выполняет это автоматически. Это директива LOCAL и она имеет следующий синтаксис:
LOCAL variable name: type, variable name: type, ...
(Любопытно. MASM все больше становится похож на Си. К чему бы это?) Давайте теперь напишем программу с использованием директивы LOCAL. Она называется Timer и требует одного параметра — time, который затем помещает в локальную переменную asm time. Из Си этот вызов будет выглядеть так:
Timer(25);
Листинг 2.5 показывает реализацию программы Timer на ассемблере, используя все директивы, которые мы обсудили в этой главе.
Листинг 2.5. Программа Timer.
.MODEL MEDIUM ;используем модель MEDIUM
.CODE ;начало кодового сегмента
;в процессе работы функция меняет содержимое регистра АХ
_Timer PROC FAR USES AX, time:WORD LOCAL asmt_time :WORD
mov AX, time
mov asm_time, AX
_Timer ENDP END
Эта программа оказалась бы куда длиннее, если б мы не использовали новые директивы MASM. Правда, если у вас есть только MASM версии 5.0, то вы можете обойтись и без них.
Совет
Я надеюсь, что вы создадите свои шаблоны, позволяющие обращаться к передаваемым параметрам и локальным переменным.
Директива USES
Надо сказать, что ассемблер MASM, начиная с версии 5.1, имеет некоторые новые директивы, упрощающие порядок передачи параметров и создания фрейма стека. Для этого вы можете использовать директиву USES вместе с директивой PROC. Они сообщат ассемблеру, какие именно регистры будут использоваться в функции. Директива USES оберегает вас от всей рутины, связанной с определением стекового фрейма и подстановками переменных. Более того, она генерирует код пролога и эпилога для сохранения регистров, которые вы указали для использования в функциях. Таким образом, содержимое этих регистров не будет изменено, когда процедура вернет управление вызвавшей ее Си-функции.
Внимание!
Помните, что Си и ассемблер используют одни и те же регистры процессора. Если вы пользуетесь регистром в ассемблерной программе, то должны его сохранить в стеке и восстановить перед завершением функции. Иначе, ваша Си-программа может просто "сломаться" в момент выхода из вызова ассемблерной вставки.
Директива PROC и относящийся к ней уточнитель USES имеет следующий синтаксис.
label PROC [[attributes]] [[USES register_list]] [[,]]
[[parameter list][:type]]...]]
§
Поле label — это имя процедуры;
§ Поле attributes сообщает ассемблеру свойства вашей процедуры. Она может содержать множество параметров, таких как тип процедуры (NEAR или FAR), «видимость» процедуры (PUBLIC или PRIVATE) и, наконец, тип языка (С, PASCAL и т. д.). Эта возможность делает наши программы на ассемблере более читаемыми. Правда, это связывает руки, но зато программы обретают определенную элегантность;
§ Поле register_list показывает, какие регистры будет использовать функция. При этом ассемблер генерирует код, который может сохранить их на время работы процедуры и восстановить при выходе;
§ Поле parameter_list очень похоже на список параметров в Си;
Для каждой передаваемой процедуре переменной должен быть указан тип, определяющий их размер (например, BYTE или WORD).
Тип задается в поле type.
Если вы пишите процедуру, в которую передаете три целых величины, и будете использовать регистры SI, DI и СХ, то должны включить следующий оператор:
far proc USES SI DI СХ, integer_1:WORD, integer_2:WORD,
integer_3:WORD
Используя директивы PROC и USES, давайте перепишем процедуру из Листинга 2.2.
Листинг 2.3. Модифицированная версия Add_Int.
.MODEL MEDIUM,С ; использовать модель MEDIUM ; и соглашения по вызову Си
.CODE ; начало кода
PUBLIC _Add_Int ; объявляем функцию как общедоступную
_Add_lnt PROC USES integer_1 :WORD, integer_2 :WORD
mov AX,integer_l ; загрузить первый операнд в AХ
add AX,integer_2 ; сложить второй операнд с AХ
_Add_Int ENDP ; конец процедуры
END ; конец кода
Как видно из Листинга 2.3, тяжкое бремя сохранения регистра ВР, создания и уничтожения стекового фрейма теперь отдано на откуп ассемблеру. Более того мы получили прямой доступ к параметрам integer 1 и integer 2.
Для кого вы пишете игру?
Подумайте, для кого предназначена ваша игра: для детей, подростков, или взрослых? Детские игры обычно выполняются в сочных, красочных тонах. Вспомните, что детей особенно привлекают мультфильмы, цирк и парады. Во всем этом присутствуют яркие краски. Подросткам обычно нравятся игры со множеством жутких монстров — чем больше, тем лучше. В играх, предназначенных для более старшей аудитории,, наибольшее внимание, как правило уделяется содержанию игры.
Дублирующая буферизация
Мы уже вкратце познакомились с понятием дублирующей буферизации в пятой главе, «Секреты VGA-карт». Теперь я хочу познакомить вас с деталями, и на это у меля есть причина.
Дублирующая буферизация используется для уменьшения мерцания экрана, вызванного перерисовкой пикселей. Дублирующий буфер — это область памяти вне видеобуфера, которая используется для построения изображения. Затем, когда изображение построено, и рисунок полностью готов к визуализации, высокоскоростной фрагмент кода перебрасывает его из дублирующего буфера в видеопамять. Обычно для этого используется команда процессора STOSW, которая перемещает несколько слов за один раз. Это наиболее эффективный способ доступа к видеобуферу.
Возможно, вам захочется узнать, работает ли дублирующий буфер медленнее чем видеобуфер? Информация, приведенная ниже, поможет вам ответить на этот вопрос:
§
Во-первых, видеопамять работает крайне медленно. Операции записи и считывания осуществляются в 2-10 раз медленнее, чем при работе с оперативной памятью;
§ Во-вторых, построив экранное изображение в дублирующем буфере, мы можем переместить все его 64000 байт за один прием с помощью команды STOSW;
§ Наконец, если мы будем выводить все изображения непосредственно в видеобуфер, то сможем увидеть на экране весь спектр мерцаний и искажений.
Подытоживая все сказанное, можно сделать вывод, что обращение к дублирующему буферу обычно происходит быстрее, чем прямой доступ к видеопамяти, причем и анимация, и графика будут выглядеть на порядок более плавными. Единственный недостаток, это то, что дублирующему буферу требуется 64000 байт памяти, но он того стоит.
С другой стороны, обычно нет необходимости буферизировать весь экран. Если окно игрового пространства имеет только 100 пикселей в высоту, то для дублирующего буфера потребуется зарезервировать лишь 100 рядов по 320 пикселей, что составит 32К. В четырнадцатой главе в качестве примера игр для нескольких участников, связанных между собой по модему, мы создадим игру, Net-Tank.
В ней мы используем дублирующий буфер только для первых 176 строк дисплея, так как остальную часть экрана будет занимать статичное, один-единственный раз созданное изображение.
Теперь, когда мы знаем, что такое дублирующий буфер и как его использовать, давайте рассмотрим некоторые примеры того, как он создается и как он действует. Модифицируем и расширим исходные программы двухмерной графики из пятой главы, «Секреты VGA-карт», использовав вместо видеопамяти дублирующий буфер (см. Листинг 7.4). Для этого в каждой функции заменим video_buffer на double_buffer. Программа CIRCLES.С делает следующее:
§ Резервирует память под двойной буфер;
§ Рисует в нем 1000 окружностей;
§ Копирует содержимое буфера на экран за один раз.
Листинг 7.4. Программа CIRCLES.С.
// ВКЛЮЧАЕМЫЕ ФАЙЛЫ ////////////////////////////////////
#include <stdio.h>
#include <math.h>
#include <graph.h>
#include <malloc.h>
#include <memory.h>
#include <string.h>
// ОПРЕДЕЛЕНИЯ //////////////////////////////////
#define SCREEN_WIDTH (unsigned int)320
#define SCREEN_HEIGHT (unsigned int}200
// ГЛОБАЛЬНЫЕ
ПЕРЕМЕННЫЕ
//////////////////////////////////////
unsigned char far *video_buffer = (char far *)0xA0000000L;// указатель
на видеобуфер
unsigned char far *double_buffer = NULL;
// ФУНКЦИИ /////////////////////////////////////////////////////////////
void Init_Double_Buffer(void)
{
double_buffer=(char far *)_fmalloc(SCREEN_WIDTH*SCREEN_HEIGHT+1) ;
_fmemset(double_buffer, 0, SCREEK_WIDTH*SCREEN_HEIGHT+l) ;
} // конец Init_Double_Buffer
/////////////////////////////////////////////////////////////
void Show_Double_Buffer(char far *buffer)
{
// копирование дублирующего буфера в видеобуфер
asm {
push ds // сохранение регистра сегмента данных
les di, video_buffer // вывод в видеобуфер...
lds si, buffer // ...из дублирующего буфера
mov сх,320*200/2 // количество перемещаемых слов
cld
rep movsw // перемещение
pop ds // восстановление регистра сегмента данных
}
} // конец Show_Double_Buffer
////////////////////////////////////////////////////////////
void Plot_Pixel_Fast_D(int x,int y,unsigned char color)
{
// вывод,пикселей в дублирующий буфер
// используется тот факт, что 320*у = 256*у + 64*у = y<<8 + у<<6
double_buffer[((у<<8) + (у<<6)) + х] = color;
} // конец Plot_Pixel_Fast_D
////////////////////////////////////////////////////////////
void Circles(void)
{
// Эта функция рисует 1000 окружностей в дублирующем буфере
// В реальной игре мы никогда не стали бы использовать столь
// нелепый алгоритм рисования окружности, а применили бы что-нибудь
// вроде таблицы выбора или другой эффективный способ. Здесь же нам
// просто надо хоть что-то нарисовать в дублирующем буфере.
int index,xo,yo,radius,x,у,color,ang;
// рисуем 1000 окружностей в случайной позиции,
// случайного цвета и размера
for (index=0; index<1000; index++)
{
// получаем параметры для следующей окружности
хо = 20 + rand()%300;
уо = 20 + rand()%180;
radius = 1 + rand()%20;
color = rand()%256i
for (ang=0; ang<360; ang++)
{
x = хо + cos(ang*3.14/180} * radius;
у = уо + sin(ang*3.14/180} * radius;
Plot_Pixel_Fast_D(x,y,(unsigned char)color};
} // конец внутреннего цикла
} // конец внешнего цикла
} // конец Circles
// ОСНОВНАЯ ПРОГРАММА //////////////////////////////////////
void main(void) {
// установка видеорежима 320х200х256
_setvideomode(_MRES256COLOR) ;
// создание дублирующего буфера и очистка его
Init_Double_Buffer() ;
_settextposition (0, 0) ;
printf("Drawing 1000 circles to double buffer. \nPlease wait...");
// построение окружностей в дублирующем буфере
Circles () ;
printf("Done, press any key.");
//ожидание нажатия клавиши, прежде,чем перебросить
// окружности на экран
getch();
ShowDoubleBuffer(double_buffer);
_settextposition(0,0) ;
printf(" That was quick. Hit any key to exit.");
// ожидание нажатия клавиши
getch ();
// восстановление первоначального видеорежима
_setvideomode(_DEFAULTMODE);
} // конец функции main
Конечно, эта программа не самая захватывающая в мире, но она дает четкое представление о том, что нас интересует. Обратите внимание, что функция вывода пикселя записывает его вместо видеобуфера в дублирующий буфер.
Использование дублирующего буфера практически убирает мерцание экрана. Однако мы не можем гарантировать, что копирование данных в видеобуфер не начнется в момент обращения аппаратуры к экранной области памяти, и в этом случае на экране могут возникнуть искажения. В следующем разделе обсуждается то, как этого можно избежать.
Дублирующее буферизирование
Дублирующее буферизирование — это метод, позволяющий избежать мерцания, которое может возникать при перемещении объектов. Когда функции, которые мы пишем, рисуют спрайты па экране, то они это делают, не учитывая статус состояния VGA-карты, то есть в них отсутствует синхронизация с дисплеем. Существует два способа уменьшить возможное мигание.
Первый метод называется дублирующим буферизированием. При этом экранное изображение формируется в памяти, а затем копируется в видеобуфер. Это позволяет минимизировать перемещение маленьких участков в видеопамяти, поскольку сразу перемещается один блок. Пример дублирующего буферизирования изображен на рисунке 5.14.
Переключение страниц - это видоизмененное дублирующее буферизирование. При использовании этого метода две видеостраницы находятся во взаимосвязи. Когда одна из них воспроизводится на экране, другая перерисовывается. Новая страница затем воспроизводится переключением указателя на нее. Обратите внимание на рисунок 5.15 для осмысления этого метода.
Оба этих метода имеют общий недостаток - уменьшение быстродействия в два раза, но при этом отсутствует мигание и достигается очень высокое качество анимации. Мы обратимся к этому методу еще раз в седьмой главе, «Продвинутая битовая графика и специальные эффекты». Сейчас поговорим об уменьшении мерцания.
Джойстик
Одному богу известно, за что джойстик получил столь неуклюжее имя. Интерфейс джойстика с ПК тоже нельзя назвать продуманным, да и аппаратная часть весьма неудобна (правда, здесь не все могут согласиться со мной). Таким образом, по сравнению с другими компьютерами работа с джойстиком на ПК весьма не ортодоксальна и противоречива, но и не так сложна, как может показаться на первый взгляд. Как видно из рисунка 3.1, джойстик - это аналоговое устройство,
которое изменяет значение сигнала на выходе в зависимости от положения рукоятки.
Наша задача состоит в преобразовании этого аналогового сигнала (его величины) в более приемлемый вид, а именно, в цифровое значение. Все мы, конечно, знаем про АЦП, и если бы ПК создавали сегодня, то он непременно стоял бы в каждой карте порта джойстика.
Но в конце 70-х, начале 80-х годов, когда ПК только разрабатывались, все АЦП были очень дороги. Тогда инженеры создали специальный АЦП только для контроллера джойстика. Для того времени это было просто гениальным решением, но сегодня заставляет ломать голову над программированием джойстика каждого программиста,
Единичная матрица
Прежде чем закончить говорить о матрицах, скажем еще об одной вещи: о единичной матрице- Не углубляясь в математические термины, я хочу сказать, что нам нужна такая матрица, умножая на которую мы получали бы исходную матрицу.
Говоря попросту, нам нужно иметь матрицу размерностью mхn, которую назовем матрицей I. Умножая на нее любую другую матрицу, мы должны получить исходную. Этой матрицей будет квадратная матрица, по главной диагонали которой записаны единицы, а все остальные элементы равны нулю:
Если мы умножим матрицу А на матрицу I,то результатом будет исходная матрица А;
А x I = A
Еще кое-что о режиме 13h
Прежде чем заняться разработкой первой программы, реализующей параллакс давайте еще раз поговорим об избранном видеорежиме. Как мы уже обсуждали в пятой главе «Секреты VGA-карт», наиболее популярным видеорежимом для программирования игр является режим 13h. Причиной номер 1 является его совместимость с различными микросхемами VGA. Другая причина, вероятно заключается в простоте программирования. Все нижеследующее является кратким обзором приемов программирования VGA в режиме 13h. Для более глубокого обзора необходимо вернуться к пятой главе. С другой стороны, если вы в совершенстве овладели программированием VGA-карт, то без ущерба можете пропустить этот раздел.
Режим 13h поддерживает разрешение 320х200 пикселей при одновременном отображении 256 цветов, где каждый пиксель представлен одним байтом. Следовательно, видеобуфер имеет размер 64000 байт. Эти 64К отображаются в область памяти, начиная с адреса А000:0000.
Все, что требуется для изображения пикселя в режиме 13h, это записать в видеобуфер байт. Проще всего это сделать, используя следующий код:
char far *VideoMem =MK_FP(0xA000,0) ;
Чтобы изобразить отдельный пиксель в позиции (200,100) с цветовым значением равным 2, вам необходимо только рассчитать смещение байта относительно начала видеопамяти. Смещение вычисляется путем умножения Y-координаты на 320 (поскольку в строке 320 пикселей) и добавлением Х-координаты:
PixelOffset = у * 320 + х;
После этого вы можете обращаться к видеопамяти как к элементам обычного одномерного массива:
VideoMem[PixelOffset] = 2;
Используя функцию memcpyO из стандартной библиотеки Си, можно разом вывести на экран целую строку пикселей. Эта функция является одним из быстрейших путей копирования данных из одного места памяти в другое. В некоторых компиляторах эти функции бывают встроенными, что значительно увеличивает скорость их выполнения.
Например, следующий фрагмент копирует 320 байт из массива Scr в видеобуфер:
memcpy(VideoMem, Scr, 320);
Поскольку VideoMem ссылается на начало видеопамяти, эта функция изображает одну полную строку пикселей на экране дисплея.
Вся графика сначала выводится в буфер, расположенный в системной памяти (в дублирующий буфер), а затем, когда кадр полностью сформирован, он копируется на дисплей единым блоком также с помощью функции memcpy().
Это все, что необходимо на данный момент времени знать по поводу режима 13h, так что двинемся дальше.
Если б только у меня было оружие и еда! (разработка объекта)
На свете не существует игр, в коих отсутствуют какие-либо предметы, которые нужно найти, подобрать и как-то использовать во время вашего приключения. Должно быть, это трудная задача — драться с атакующими вас чудовищами, не имея ни оружия для отражения нападения, ни еды для пополнения сил. Кроме того, голые стены без малейшего намека на какую-либо обстановку также будут выглядеть довольно уныло, поэтому комнаты желательно заполнить какими-нибудь объектами.
Разработка объектов похожа на создание «кирпичиков» стен с одной только существенной разницей: в изображениях обязательно будут присутствовал невидимые или прозрачные области. .Каждый «кирпичик» стены представляет собой прямоугольник, все пиксели которого обычно отображаются на экране. Правда в некоторых играх встречаются перегородки с прозрачными областями. Это могут быть различные ворота, решетки, окна. Такие же «кирпичики» используются и для изображения открывающихся и закрывающихся дверей.
В процессе разработки объекты также представляются спрайтами прямоугольной формы, но те части, которые не должны быть видимы, заполняются «прозрачным» цветом. Поскольку «прозрачные» пиксели на экран не выводятся то сквозь эти участки будет виден находящийся под объектом фон.
Соотношение размеров объектов и стен
При разработке объектов их создают такими, чтобы на фоне стен они выглядели достаточно пропорционально. Учитывая, что средняя высота стены равна примерно 8 футам, а «кирпичики» стен имеют размер 64 пикселя, можно подсчитать, что на каждый фут приходится по 8 пикселей (64 пикселя разделить на 8 футов). Это правило хорошо работает для больших объектов, таких как мебель или стволы деревьев, однако для мелких предметов оно не выполняется. Было бы очень непросто нарисовать пистолет, имеющий размер всего около 8 дюймов, впихнув его в изображение высотой 5 или 6 пикселей. В этом случае можно применить некоторую художественную вольность. Просто рисуйте объекты такими маленькими, насколько это возможно, чтобы можно было различить детали, и не обращайте внимания на пропорции.
Помните о перспективе
Обратите внимание, что когда вы гуляете по коридорам вашего трехмерного пространства, поверхность земли или пола располагается немного под углом к горизонту. Внесение небольшого искажения в форму «кирпичиков» может существенно улучшить реалистичность восприятия. Если вы сделали своего персонажа достаточно высоким, чтобы он мог смотреть поверх стен, то можно изобразить открывающийся перед ним вид под несколько большим углом к горизонту. Однако постарайтесь не переборщить с перспективой, иначе это не будет согласовываться с остальным окружением.
Фазы создания видеоигр
Видеоигра, как и любой другой программный продукт, должна создаваться по определенной методике. Это значит, что мы в процессе разработки должны придерживаться определенных правил и рекомендаций. Итак:
§
Во-первых, нужна идея. Мы уже об этом говорили;
§ Если есть понимание того, что будет в игре, то есть смысл написать что-то типа сценария. Если игра будет развиваться на нескольких уровнях — опишите каждый из них;
§ Затем вам надо разнообразить каждый из уровней какими-нибудь неожиданными ходами, целями и т. д. Вы должны заинтересовать игрока, заставить его проходить уровень за уровнем в вашей игре;
§ Если у вас есть понимание каждого уровня игры, то имеет смысл подумать о структуре самой игры. Как будут вести себя игровые объекты, как они будут взаимодействовать, какие возможности получит игрок?
В этот момент у вас уже есть достаточно информации, чтобы садиться и начинать писать более развернутый план игры. Теперь попробуйте чуть более заострить свое внимание на специфике игры. Например:
§ Выберите, в каком видеорежиме у вас будет работать игра. Например, она может быть выполнена в режиме высокого разрешения, но использовать при этом только несколько цветов.
§ Подумайте, насколько сложной будет графика. Будет ли она трехмерной или двухмерной.
О том, как решать эти проблемы вы также узнаете из данной книги.
Когда вы решите для себя эти вопросы, настанет время подумать о тех средствах созидания, которыми мы располагаем. Попробуйте начать конструировать с максимальной детализацией самый первый уровень. У вас сразу появится необходимость в специальных инструментальных средствах. Вот их минимальный набор:
§ Программа для рисования битовых образов;
§ Программа для анимации битовых образов;
§ Си-код для бит-блиттинга (блокового перемещения битовых образов), изменения видимого размера объектов (масштабирования) и рисования линий;
§ Алгоритмы искусственного интеллекта для персонажей игры;
§ Средства для работы со звуком;
§ Си-код для работы с устройствами, ввода;
§ Инструменты для рисования уровней и сохранения их на диске;
§ Наборы MIDI-звуков для каждого из уровней.
§ Когда вы начнете писать программу, старайтесь разбить ее на маленькие секции. На самом деле программа может быть разбита на следующие куски:
§ Игровой мир и описывающие его структуры данных;
§ Система рендеринга;
§ Система ввода/вывода;
§ Система искусственного интеллекта;
§ Основной игровой цикл; Интерфейс пользователя;
§ Система звука.
§ Система искусственного интеллекта;
Флаговый регистр
Этот регистр сохраняет статусы состояния процессора, такие как: Z (zero - ноль), С (carry - перенос) и т. д. Этот регистр не доступен напрямую, но его содержимое можно узнать с помощью соответствующих инструкций.
Фоновая мультипликация
Мы просто не можем пройти мимо фоновой мультипликации. Компьютерные Игры в плане эффектов обошли своих предшественников — игры с фишками. Добавление небольших мультиков и эффектов, которые впрямую не связаны с Действиями в игре делает компьютерные игры забавнее и оригинальнее.
Хороший пример этому — оформление в игре Karate. Летающие птички, время от времени садящиеся на вершину действующего вулкана, — это как раз то, что надо. Для игры, созданием которой мы займемся в конце этой книги, я Придумал светлячков, которые просто мигают то тут, то там. Они не служат для Какой-то определенной цели, а просто делают мир игры немного более реалистичным и полным.
Эти небольшие, длящиеся не более минуты эпизоды вы должны добавить Уже после того, как игра сделана. Всегда пытайтесь добавить в оформление игры Немного небольших мультфильмов и зрительных эффектов — это придаст вашей игре своеобразие.
Функции ответа
Название этой функции - функция ответа — целиком отвечает ее назначению. Эта функция реагирует на то, что произошло какое-то событие: наступило определенное время, изменилось значение переменной или что-то еще. С помощью функций ответа мы будем реагировать на события или состояния определенными действиями.
Например, с помощью такой функции мы могли бы следить за временем и каждые пять минут воспроизводить какой-либо звук. В данном случае функция ответа реагирует на то, что пять минут прошли, издавая звуковой сигнал. С другой стороны, мы могли бы написать и такую функцию ответа, которая следила бы за переменной, значение коей может меняться либо самой программой либо во время обработки прерывания, и реагировала бы на такое изменение.
На рисунке 12.5 показано взаимодействие функций ответа с другими частями системы.
Все, о чем мы сейчас говорим, относится к методам программирования задач "реального времени". Обычно их не обсуждают ни в книгах, ни в институтах (возможно, поэтому они и кажутся вам несколько странными).
Но не будем отвлекаться и напишем функцию ответа, следящую за переменной timer. Через каждые пять «тиков» системных часов эта функция обнуляет значение переменной timer и изображает в случайной точке экрана пиксель. Текст этой программы приведен в Листинге 12.4.
Листинг 12.4. Функция ответа (PRES.C).
// ВКЛЮЧАЕМЫЕ ФАЙЛЫ ////////////////////////////////////////
#include <dos.h>
#include <bios.h>
#include <stdio.h>
#include <math.h>
#include <conio.h>
#include <graph.h>
// ОПРЕДЕЛЕНИЯ /////////////////////////////////
#define time_keeper_int 0x1C
// ГЛОБАЛЬНЫЕ ПЕРЕМЕННЫЕ /////////////////////////////////////////
void (_interrupt _far *01d_Isr)();
// хранит Старый обработчик прерывания
long time=0;
// функции //////////////////////////////////////////////
void _interrupt _far*Timer ()
{
// увеличивает глобальную переменную
// еще раз отметим, что мы можем это делать, так как при входе в
// процедуру обработки прерывания регистр DS
указывает-на сегмент
// глобальных данных нашей программы
time++;
} // конец Timer
////////////////////////////////////////////
void Plot_Responder(void)
{
static int first_time=l;
static long old_time;
// проверка первого вызова
if (first_time)
{
//сброс флага первого вызова
first_time=0;
old_time = time;
} // конец оператора if else
( // не первый вызов
// прошло ли пять "тиков"?
if ( (time-old_time)>=5)
{
old_time
= time; // сохранить новую точку отсчета
// вывод пикселя на экран
_setcolor(rand()%16);
_setpixel(rand()%320,rand()%200) ;
} // конец оператора if
} // конец else
}// конец Plot_Responder,
// ОСНОВНАЯ ПРОГРАММА /////////////////////////////////////////////
main()
{
_setvideomode(_MRES256COLOR);
printf('Hit any key to exit...");
// установка процедуры обработки прерывания
0ld_Isr = _dos_getvect(TIME_KEEPER_INT);
_dos_setvect(TIME_KEEPER_INT, Timer);
// ожидание нажатия клавиши пользователем
while(!kbhit())
// ... текст программы игры
// вызов всех функций ответа Plot_Responder() ;
// ... текст программы игры
} // конец while
_setvideomode(_DEFAULTMODE) ;
// восстановление старого обработчика прерывания
_dos_setvect(TIME_KEEPER_INT, 01d_Isr) ;
} // конец функции main
В программе, приведенной в Листинге 12.4, функция ответа следит за временем и каждые пять «тиков» системных часов или 5х55.4 миллисекунды рисует на экране пиксель. Значение 55.4 получается из того, что системный таймер по умолчанию настроен так, что генерирует прерывание 18.2 раза в секунду или каждые 55.4 миллисекунды. Обратите внимание и на то, что функция ответа автономна.
Автономные функции и функции ответа могут быть использованы для самых разнообразных целей. Они позволяют выделить часть задач, не связанных с непосредственным ходом игры в отдельную группу и решать их отдельно. Логика игры и так достаточно сложна - поэтому не стоит ее без необходимости загромождать мерцающими огоньками и прочими мелкими деталями.
На персональных компьютерах следить за временем и,управлять событиями связанными с ним, можно только с помощью внутренних таймеров системы. К сожалению, большинство из них используется для других целей. Однако один применяется специально для внутренних часов компьютера. Он «тикает» со тростью 18.2 раза в секунду. Что и говорить, необычная отправная точка для вычислений и, честно говоря, я понятия не имею, почему создатели персональных компьютеров выбрали именно это значение. Но мы можем перепрограммировать его и задать более разумное число, например, 20, 30 или 60 «тиков» в секунду. Естественно, такие значения удобнее использовать в нашей компьютерной игре. Итак, давайте разберемся, как можно перепрограммировать внутренний таймер системы.
Функциональное описание программы WarEdit
Программа WarEdit исключительно проста для понимания. В основном, бопьшинство инструментов не слишком сложно создать и их описание только отнимает время. Единственная убийственно сложная вещь — это графический интерфейс Поскольку мы используем DOS, а не Windows, то должны создать все сами. Например, если мы хотим увидеть в программе кнопки, придется написать функции для их изображения и обслуживания. То же самое относится и ко всем прочим элементам интерфейса. Тут уж ничего не поделаешь, с этим нужно считаться.
Если вы собираетесь изготавливать инструменты с графическим интерфейсом, лучше начать с создания библиотеки, которой вы сможете доверять. Для этого вам придется написать тысячи строк программы.
Попробуем подойти к процессу дизайна с другой стороны. Посмотрим, что необходимо для воплощения конкретно WarEdit'a. Затем мы охватим несколько наиболее интересных функций.
WarEdit был разработан для создания игрового пространства Warlock, основываясь на тех текстурах и объектах, которые могут присутствовать в данной игре. Нам нужно иметь возможность загружать и сохранять уровни, а также просматривать предварительное изображение текстур. Я сознаю, что для большинства людей отдельные пиксели для представления блоков покажутся слишком мелкими, так как цвет одного пикселя трудно будет отличить от цвета другого, стоящего рядом. Приняв это во внимание, я решил создать окно детализации изображения, которое должно помочь различать близлежащие объекты. Нам понадобятся следующие функции:
§
Для загрузки PCX-файлов;
§ Для рисования спрайтов и масштабирования объектов;
§ Библиотека функций мыши;
§ Диалоговые окна;
§ Процедура определяющая положение курсора мыши в момент щелчка;
§ Функция для детализации изображения;
§ Некоторые функции ввода/вывода для загрузки и-сохранения данных;
§ Интерфейс пользователя.
Мы уже написали первые три функции. Остальные я собираюсь создавать от нуля или делая добавления к уже имеющейся программе.
Давайте начнем с последнего. Я догадывался, что на разработку алгоритма графического интерфейса у меня уйдет, по меньшей мере, несколько дней. Поэтому я решил просто нарисовать его в редакторе Deluxe Paint и загрузят в виде PCX-файла. Закончив изготовление интерфейса, я записал местоположение размеры всех окон и образов, чтобы впоследствии задать все интересующие области в виде констант. Самым сложным моментом был выбор цветов. Я решил пользовать в игре четыре различных типа стенок, причем так, чтобы каждый этих типов мог быть представлен шестью видами текстур. Поэтому для рисования стен можно выбрать четыре различных цвета: серый, красный, синий, зеленый, а для представления текстур подобрать оттенки этих основных цветов.
Зная, что пользователю может показаться трудным различать на экране каждый из оттенков, я решил создать окно детализации изображения. Небольшая площадь редактируемого игрового пространства вокруг курсора мыши захватывается и в увеличенном масштабе помещается в окно детализации. При этом каждый пиксель изображается квадратиком 3х3 пикселя.
При выборе цвета, представляющего собой определенную текстуру или объект, вы видите изображение выбранного объекта в окне предварительного просмотра. Это выполняется посредством взятия битовой карты текстуры или объекта и изменения ее масштаба до необходимого размера. Теперь цветом можно рисовать изображения. Однако когда вы рисуете объекты игрового пространства, в базу данных передается не само значение цвета. С использованием справочной таблицы цвет переводится в значение, распознаваемое программным кодом игры, как обычные данные текстуры или объекта. Листинг 15.1 содержит определения типов этих данных.
Листинг 15.1. Секция определений WarEdit.
#define walls START 64
#define NUM_WALLS 24
#define DOORS START 128
#define NUM_DOORS 4
#define SCROLLS_START 144
#define NUM_SCROLLS 4
#define potions_START 160
#define NUM_POTIONS 4
#define foods_start 176
#define num_foods 2
#define MONSTERS_START 192
#define num_monsters 2
#define wall_STONE_1 (WALLS_START+0) // Пока
только 6
#define WALL_STONE_2 (WALLS_START+1)
#define WALL_STONE_3 (WALLS_START+2)
#define WALL_STONE_4 (WALLS_START+3)
#define WALL_STONE_5 (WALLS_START+4)
#define WALL_STONE_6. (WALLS_START+5)
#define NUM_STONE_WALLS 6
#define WALL_MELT_1 (WALLS_START+6) // Пока только 6
#define WALL_MELT_2 (WALL3_START+7)
#define WALL_MELT_3 (WALLS_START+8)
#define WALL_MELT_4 (WALLS_START+9)
#define WALL_MELT__5 (WALLS_START+10)
#define WALL_MELT_6 (WALLS__START+11)
#define NUM_MELT_WALLS 6
#define WALL_OOZ_1 (WALLS_START+12) // Пока только 6
#define WALL_OOZ_2 (WALLS_START+13)
#define WALL_OOZ_3 (WALLS_START+14)
#define WALL_OOZ_4 (WALLS_START+15)
#define WALL_OOZ_5 (WALLS_START+16)
#define WALL_OOZ_6 (WALLS_START+17)
#define NUM_OOZ_WALLS 6
#define WALL_ICE_1 (WALLS_START+18) // Пока только 6
#define WALL_ICE_2 (WALLS_START+19)
#define WALL_ICE_3 (WALLS_START+20)
#define WALL_ICE_4 (WALLS_START+21)
#define WALL_ICE_5 (WALLS_START+22)
#define WALL_ICE_6 (WALLS_START+23)
#define NUM_ICE_WALLS б
#define DOORS_1 (DOORS_START+0) // Пока только 4
#define DOORS_2 (DOORS_START+1)
#define DOORS_3 (DOORS_START+2)
#define DOORS_4 (DOORS_START+3)
#define SCROLLS_1 (SCROLLS_START+0) // Пока только 4
#define SCROLLS_2 (SCROLLS_START+1)
#define SCROLLS_3 (SCROLLS_START+2)
#define SCROLLS_4 (SCROLLS_START+3)
#define РОТIONS_1 (POTIONS_START+0) // Пока только 4
#define POTIONS_2 (POTIONS_START+1)
#define POTIONS_3 (POTIONS_START+2)
#define POTIONS_4 (POTIONS_START+3)
#define FOODS_1 (FOODS_START+0) // Пока только 2
#define FOODS_2 (FOODS_START+1)
#define MONSTERS_1 (MONSTERS_START+0) // Пока только 2
#define MONSTERS_2 (MONSTERS_START+1)
#define GAME_START 255 // точка, из которой игрок
// начинает свой путь
Как можно видеть из Листинга 15.1, для каждой текстуры или объекта игрового пространства выделен некоторый диапазон значений. К примеру, все стены будут кодироваться числами от 64 до 127. В данный момент существует только 64 стены, но я оставил в программе место еще для 64 стен. Такой же подход применен и для определения всех остальных игровых объектов, так что структуру данных можно улучшить без особого труда. Когда вы рисуете карту игрового поля, то в действительности заполняете базу данных. Она представляет собой длинный массив, по способу доступа напоминающий двухмерную матрицу. Когда массив построен и уровень закончен, мы должны как-то сохранить данные. Для этого предусмотрены кнопки LOAD и SAVE. При нажатии одной из них, появляется диалоговое окно, запрашивающее подтверждение команды. Если вы выбрали YES, с помощью стандартной функции fopen() открывается файл, после чего данные сохраняются в файле или загружаются из файла. Но предварительно нужно ввести имя файла, используя строчный редактор. Этот редактор мне пришлось сделать самостоятельно. Он очень прост и понимает только алфавитно-цифровые клавиши.
Примечание
Мне пришлось написать такой редактор самостоятельно, поскольку единственный способ ввода строки, предлагаемый языком Си - это функция scanf (). Однако использование этой функции чревато неприятностями: при наборе возможны ошибки, текст может выйти за границы диалогового окна и т. д.
Давайте поговорим о диалоговом окне, о котором я только что упоминал. Оно представляет собой PCX-файл с двумя нарисованными кнопками.
Хотя кнопки, конечно же, являются обычными картинками, я точно знаю их расположение относительно левого верхнего угла. Все, что мне нужно сделать, это доверить, находится ли мышь над кнопкой и щелкнули ли вы мышью. Для осуществления этого теста я создал универсальную функцию, которой нужно передать следующие параметры:
§ Размер каждой кнопки;
§ Количество кнопок в столбце и строке;
§ Местоположение первой кнопки;
§ Расстояние между кнопками.
Эта функция будет определять, какая из кнопок нажата. (Вспомните, не существует легкого пути для получения подобных вещей. Мы все должны сделать собственноручно. У нас нет приличного менеджера окон и механизма посылающего нам сообщения при нажатии кнопок. Хотя вы можете попробовать написать его сами, поскольку в мои планы это не входит.) В Листинге 15.2 показана функция, которая обнаруживает нажатие кнопок.
Листинг 15.2. Обнаружение нажатия кнопки.
int Icon_Hit(int хо, int yo, int dx, int dy,
int width, int height,
int num_columns, int num_rows,
int mx, int my)
{
// получая геометрические установки кнопок, эта функция вычисляет,
// по которой из них щелкнули мышью
int row, column, xs,ys,xe,ye;
for (row=0; row<num_rows; row++)
{
// вычислить начало и конец Y-координаты колонки
ys = row*dy + yo;
ye = ys+height;
for(column=0; column<num_columns; column++)
{
xs = column*dx + xo;
xe = xs+width;
//Проверить, находится ли курсор мыши в области кнопки
if (mx>xs && mx<xe && my>ys && my<ye)
{
return(column + row*num_columns);
} // конец if
} // конец внутреннего цикла
} // конец внешнего цикла
return(-1); // не нажата
} // конец функции
Это все относительно редактора WarEdit. Я предлагаю вам поиграть с программой, чтобы получить представление о ней, прежде чем мы начнем разговор об улучшениях, которые могут быть внесены в редактор.
Наконец, для создания работающей версии WarEdit желательно использовать два объектных файла, называемых GRAPH0.OBJ и MOUSELIB.OBJ. Я предлагаю объединить их в библиотеку и затем связать с WEDIT.C. (Хотя, конечно, вы можете поступать, как вам больше нравится). Как вы понимаете, файл GRAPH0.OBJ получается после трансляции исходников GRAPH0.C и GRAPH0.H, a MOUSELIB.OBJ - из MOUSELIB.C, о котором уже упоминалось ранее.
Гаснущее изображение
Этот эффект уже много лет пользуется популярностью у программистов, потому что получить его фантастически легко — для этого требуется всего несколько строк кода.
По существу мы медленно уменьшаем значение всех регистров цвета до тех пор, пока они не достигнут нуля. Как мы узнали в этой главе в разделе «Освещение ваших игр», лучше всего сохранять эквивалентный баланс цветов, для чего надо пропорционально увеличивать или уменьшать интенсивность каждого цвета. Однако в случае данного эффекта действие разворачивается так быстро, что цвета разобрать сложно, поэтому о цветовом балансе можно не беспокоиться.
Поэтому программа, приведенная в Листинге 7.9 просто уменьшает каждый компонент каждого цвета в пяти регистрах до тех пор, пока все три компонента — красный, зеленый и синий — не становятся равными нулю. На этом функция завершается.
Исчезновение Изображения
Этого замечательного эффекта можно достичь вообще одной строчкой. Я вам продемонстрирую ее через минуту.
Данный эффект достигается за счет образования черных дыр в пикселях изображения на экране. Все что надо сделать, это последовательно нанести на экран случайным образом тысячи черных точек. Так как экран содержит последнее изображение (комнаты, пейзажа или чего-нибудь еще), прорисовка этих черных точек приведет к тому, что создастся эффект растворения картинки в воздухе,
Здесь приведены две самые короткие строчки кода, которые мне удалось придумать:
for (index=0; index<=300000; index++)
Plot_Pixel_Fast(rand()%320, rand()%200, 0);
Этот код просто размещает 300000 черных пикселей по всему экрану. Почти наверняка такое большое количество пикселей разрушит все изображение на экране. (Мы могли бы рассчитать точное количество итераций, но это заняло бы лишнее время. Число 300000 выглядит вполне приемлемым.)
Этот эффект может быть использован для замены одного экрана другим, если в нем использовать не черные пиксели, а пиксели новой картинки, случайным образом выбирая их координаты Х и Y.
Оплывание изображения
Этот эффект появился в игре DOOM и быстро распространился по другим играм. Мой вариант не столь впечатляющий. Однако, этого и следовало ожидать, ведь я потратил на него всего 15 минут.
Таяние изображения осуществляется благодаря движению сверху вниз с разной скоростью 160 маленьких «червячков» под влиянием силы тяжести. И .Когда "червячки" движутся вниз, они поедают пиксели. Через некоторое время большинство из них достигает низа и растворение заканчивается. (Я подозреваю, Что и в DOOM использовалась подобная техника. Таким образом, каждая вертикальная лента масштабируется вместо простого стирания пикселей.)
Демонстрация смены экрана
Гаснущие, исчезающие и растворяющиеся изображения — все они выглядят неплохо. Здесь приведена программа, которая позволяет увидеть эти эффекты в действии.
Листинг 7.9. Эффекты экрана (SCREENFX.C).
// ВКЛЮЧАЕМЫЕ ФАЙЛЫ ////////////////////////////////////////
#include <io.h>
#include <conio.h>
#include <stdio.h>
#include <stdlib.h>
#include <dos.h>
#include <bios.h>
#include <fcntl.h>
#include <memory.h>
#include <malloc.h>
#include <math.h>
#include <string.h>
#include "graph0.h" // включаем нашу графическую библиотеку
// СТРУКТУРА.///////////////////////////////////////////////
typedef struct worm_typ
{
int у; // текущая Y-координата "червячка"
int color; // цвет "червячка"
int speed; // скорость "червячка"
int counter; // счетчик
}, worm, *worm_ptr;
// ГЛОБАЛЬНЫЕ
ПЕРЕМЕННЫЕ
///////////////////////////////////
unsigned int far *clock = (unsigned int far *)0x0000046C;
// указатель на внутренний таймер 18.2 "тик"/с
pcx_picture screen_fx; // наш
тестовый экран
worm worms[320]; // используется при оплывании экрана
//ФУНКЦИИ /////////////////////////////////////////////////
void Timer(int clicks)
{
// эта функция использует внутренний таймер с частотой 18.2 "тик"/с
// 32- битовое значение этого таймера имеет адрес 0000:046Ch
unsigned int now;
// получим текущее время
now = *clock;
// Ожидаем до истечения указанного периода времени.
// Заметьте, что каждый "тик" имеет длительность примерно в 55 мс
while(abs(*clock - now) < clicks){}
} // конец Timer ////////////////////////////////////////////////////////////
void Fade_Lights (void)
{ // эта функция гасит свет, медленно уменьшая значения цветов
// во всех цветовых регистрах
int index,pal_reg;
RGB_color color,color_1,color_2,color_3;
for (index=0; index<30; index++)
{
for (pal_reg=l; pal_reg<255; pal_reg++)
{
// получить затемняемый цвет
Get_Palette_Register(pal_reg,(RGB_color_ptr)&color) ;
if (color.red > 5) color.red-=3;
else
color.red = 0;
if (color.green > 5) color.green-=3;
else
color.green = 0;
if (color.blue > 5) color.blue-=3;
else
color.blue = 0;
// уменьшить интенсивность цвета
Set_Palette_Register(pal_reg,(RGB_color_ptr)&color) ;
} // конец внутреннего цикла
// немного подождем
Timer(2);
} // конец внешнего цикла } // конец Fade_Lights
///////////////////////////////////////////////////////////
void Disolve(void)
{
// "растворение" экрана рисованием биллиона черных пикселей
unsigned long index;
for (index=0; index<=300000; index++, Plot_Pixel_Fast(rand()%320, rand()%200, 0));
} // конец Disolve
//////////////////////////////////////////////////////////
void Melt(void)
{
// Функция "оплавляет" экран, двигая маленьких "червячков"
// с разной скоростью вниз по экрану. Эти "червячки" меняют
// на своем пути цвет пикселей.
int index,ticks=0;
// инициализация "червячков"
for (index=0; index<160; index++)
{
worms[index].color = Get_Pixel(index,0);
worms[index].speed = 3 + rand()%9;
worms[index].у =0;
worms[index].counter = 0;
// прорисовка "червячка"
Plot Pixel_Fast((index<<1), 0, (char) worms [index].color);
Plot_Pixel_Fast((index<<1), 1, (char) worms [index].color);
Plot_Pixel_Fast((index<<1), 2, (char) worms [index].color) ;
Plot_Pixel_Fast((index<<1) + l,0, (char) worms [index].color) ;
Plot_Pixel_Fast((index<<1) + 1,1, (char) worms [index].color) ;
Plot_Pixel_Fast((index<<1) + 1,2, (char) worms [index].color);
} // конец цикла
// плавим экран
while(++ticks<1800)
{
// работа "червячков"
for (index=0; index<320; index++)
{
// пора подвинуть "червячка"
if (++worms[index].counter == worms[index].speed)
{
// обнуляем счетчик
worms[index].counter = 0;
worms[index].color = Get_Pixel(index,worms[index],y+4);
// достиг "червячок" низа экрана?
if (worms[index].у < 193)
{ Plot_Pixel_Fast ((index<<1), worms [index] .y, 0) ;
Plot Pixel Fast ((index<<3.),worms [index] .y+1,
(char)worms[index].color);
Plot_Pixel_Fast ( (index<<1) ,worms [index] .y+2,
(char)worms[index].color);
Plot Pixel Fast ((index<<1),worms [index] .y+3,
(char)worms[index].color) ;
Plot Pixel Fast ( (index<<1) +1,worms [index] .y, 0) ;
Plot_Pixel_Fast( (index<<1)+1,worms [index] .y+1,
(char)worms[index].color);
Plot Pixel Fast ( (index<<1)+l,worms [index] .y+2,
(char)worms[index].color);
Plot_Pixel_Fast ( (index<<1)+1,worms [index] .y+3,
(char)worms[index].color);
worms[index].y++;
} // конец оператора if
} // конец оператора if
} // конец цикла // ускоряем плавление
if (!(ticks % 500))
{
for (index=0; index<160; index++) worms[index].speed--;
} // конец оператора if
} // конец оператора while
} // конец Melt
// ОСНОВНАЯ ПРОГРАММА //////////////////////////////////////
void main(void)
(
int index,
done=0,
sel;
// установка видеорежима 320х200х256
Set_Mode(VGA256);
PCX_lnit((pcx_picture_ptr)&screen_fx) ;
PCX_Load("war.pcx", (pcx_picture_ptr)&screen_fx,1);
PCX_Show_Buffer((pcx_picture_ptr) &screen_fx);
PCX_Delete((pcx_picture_ptr)&screen_fx);
_settextposition(22,0);
printf('1 - Fade Lights.\n2 - Disolve.\n3 - Meltdown.");
// какой эффект хочет увидеть игрок? switch(getch())
{
case '1': // гаснущий экран {
Fade_Lights();
} break;
case '2': // растворяющийся экран {
Disolve();
} break;
case '3': // оплывающий экран {
Melt(};
} break;
} //конец оператора switch
// возврат в текстовый режим
Set_Mode(TEXT_MODE) ;
} // конец функции main
Геометрическое моделирование
Геометрическое моделирование необходимо для придания играм большего реализма. Объекты, построенные компьютером при геометрическом моделировании, выглядит цельным. Посмотрите на рисунок 6.11, чтобы увидеть разницу между цельной геометрической и каркасной моделями.
Как видите, геометрическая модель выглядит более убедительно. Сегодня использование каркасных объектов уже нельзя назвать удовлетворительным решением. Теоретически создание цельных моделей ненамного сложнее, нежели каркасных. Мы можем использовать те же самые структуры данных, преобразования и проекции. Но когда мы пытаемся нарисовать цельный трехмерный объект, возникают определенные проблемы. Как вы понимаете, существуют еще и скрытые поверхности — те, которые не видны игроку (например, тыльная сторона рассматриваемого объекта) и которые не должны появляться на экране. Компьютер не понимает разницы между видимыми и невидимыми поверхностями. Он нарисует их, все. Чтобы избежать этого, мы должны найти способы удаления скрытых поверхностей. Сейчас мы об этом и поговорим.
Главная матрица масштабирования
Главная матрица масштабирования - это такая матрица, в которой scale_x и scale_y - это коэффициенты масштабирования объекта по координатам х и у.
Такая матрица позволяет выполнять неоднородное масштабирование — мы можем задать один масштаб по оси Х и другой — по Y. Таким образом, если мы хотим масштабировать объект однородно, то должны задать scale_x = scale_y.
Главная матрица перемещений
Главной матрицей перемещений будем называть такую матрицу, в которой x_translation и y_translation - это коэффициенты перемещения объекта по осям Х и Y. Вот как она выглядит: