Программирование игр для Windows. Советы профессионала

ИГРА WARLOCK (КОЛДУН)


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

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

Взгляните на рисунок 19.1. На нем показаны несколько кадров игры Warlock.

Постарайтесь превратить эту заготовку в настоящую игру. Может быть, у вас получится какая-нибудь простая игрушка типа «Захвата флага», а может быть, вы создадите нечто подобное DOOM'y. Что бы вы ни решили, мне будет приятно увидеть результат, поэтому в конце главы я сообщу адрес, по которому вы можете прислать любые пожелания или вопросы (я даже мог бы опубликовать вашу игру в какой-нибудь подходящей книге или журнале).

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

§          Сюжет игры Warlock;

§          Компоненты игры;

§          Новый отсекатель лучей;

§          Изображение текcтуры;

§          Оттенение;

§          Использование ассемблера;

§          Цикл игры;

§          Игровое поле;



§          Режим демонстрации;




§          Размещение объектов в пространстве;

§          Достижение некоторой скорости;

§          Несколько слов напоследок.





Сюжет игры

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

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

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

Компоненты игры

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

Примечание по звуку

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


При компиляции я сделал такие установки:

§          DMA#1

§          IRQ #5

§          I/O port-220h

Если ваши установки лучше, вы всегда можете заново оттранслировать исполняемый модуль и сделать соответствующие изменения в функции Voc в начале модуля main.

Даже если вы и не имеете звуковой карты, все равно запустите программу, чтобы увидеть ее в действии.

Загрузка и компиляция Warlock

Исполняемый модуль называется WARLOCK.EXE. Для запуска программы достаточно ввести warlock в командной строке DOS.

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

WARLOCK.С   исходная программа игрового ядра.

SNDLIB.C      исходная программа звуковой системы. Соответствующие функции вы видели раньше, но для Warlock я создал более совершенный интерфейс и разместил его в этом файле.

SNDLIB.H      файл заголовков для SNDLIB.C.

GRAPHICS.С   окончательная графическая библиотека (почти идентичная GRAPHO.С).

GRAPHICS.H   файл заголовков для GRAPHICS.C.

DRAWG.ASM   эта функция изображает черное небо и серую поверхность земли, используя 32-битовые команды.

RENDER32.ASM эта функция копирует изображение из дублирующего буфера в видеопамять. Она также использует 32-битовые команды.

SLIVER.ASM    эта функция изображает отдельный фрагмент текстуры изображения в двойном буфере. Это способ генерации трехмерного вида (набор вертикальных полосок). Я снова использовал 32-битовые команды, и таким образом вы получаете доступ к регистрам дополнительного сегмента.

После трансляции всех файлов они связываются вместе, используя warlock.с как главный модуль. Я делал вот как: сначала создал общую библиотеку, в которой объединил звуковую и графическую библиотеки, а также дополнительные функции на ассемблере. Затем я откомпилировал ее и скомпоновал с главной программой WARLOCK.С.



Учтите, что компиляция производится с использованием модели памяти MEDIUM. Также не забудьте использовать опцию /G2, позволяющую учитывать специфические особенности команд 286-го процессора. Все необходимое записано в стандартном ВАТ-файле, который я использовал для компиляции программы с помощью Microsoft C/C++ 7.00:

cl -AM -Zi -с -Fc -Gs -G2 %l.c

Эта строка переводится так: «компилировать для модели памяти MEDIUM, с использованием команд 286-го процессора, с отключенным контролем переполнения стека и генерировать листинг программы». Чтобы получить представление, как компонуется программа, взгляните на рисунок 19.2. На нем показана схема, по которой отдельные кусочки объединяются в единое целое.



Режим демонстрации     

Игра начинается в демонстрационном режиме, на это указывает надпись DEMO в верхней части экрана. Вы увидите, как демонстрационная программа «прокручивает» примерно минутную пробежку в пространстве. Когда надоест, нажмите клавишу Esc. Это позволит вам самим побегать по лабиринту.

Движение по игровому пространству

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

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

Когда вдоволь нагуляетесь, вновь нажмите Esc для выхода из игры. Вы пока побродите по закоулкам, а я подожду здесь.

Вернулись? Теперь поговорим немного по поводу каждой части игрового ядра, представляющей для нас хоть какой-нибудь интерес.

Новый отсекатель лучей

Отсекатель; лучей, который вы видели в шестой главе, «Третье измерение», по быстродействию был подобен матричному принтеру, пытающемуся напечатать полутоновую картинку с высоким разрешением! Словом, он был чрезвычайно медлительным. Так получилось потому, что он был написан совершенно прямолинейно и целиком на Си, в нем использовалась математика с плавающей запятой и не применялись справочные таблицы.


Отсекатель лучей, который я создал для Warlock, немного другой. Новая программа оптимизирована во многих частях, хотя есть возможность ускорить ее работу еще примерно в два раза. Однако после этого отсекатель лучей мог бы стать таким жутким, что вы не захотели бы иметь с ним дело. (Я сам едва могу вообразить, как он выглядел бы в этом случае, а ведь я его написал!)

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

Мы поговорим немного больше по этому поводу и о некоторых других вещах немного позже, а сейчас перейдем непосредственно к программе нового отсекателя лучей (Листинг 19-1).

Листинг 19.1. Новая программа отсечения лучей.

void Ray Caster(long x,long у,long view_angle)

{

// Эта функция является сердцем системы. Она основана на программе

// RAY.С. Обратите особое внимание на оптимизацию по скорости

// работы. Это достигается, в основном, за счет использования таблиц

// поиска и применения математики с фиксированной запятой.

int   cell_x,      // Текущие координаты луча

сеll_у,

ray,         // Обрабатываемый луч

casting=2,   // Указывает, обработаны ли обе координаты

х_hit_type, // Указывают на тип объекта, пересеченного

у_hit_type, // лучом. Используется при выборе текстуры

х_bound,     // Следующая точка пересечения луча

y_bound,

next_y_cell, // Используется при отрисовке ячейки, в

next_x_cell, // которой находится луч

xray=0,      // Счетчик проверенных точек пересечения

// с вертикалями плана

yray=0,  // Счетчик проверенных точек пересечения

// с горизонталями плана

х_delta,     // Смещение, которое необходимо добавить

у_delta,     // для перехода к следующей ячейке

xb_save,

yb_save,

xi_save,     // Используется для сохранения координат

yi_save,     // точек пересечения

scale;

long  cast=0,

dist_x,      // Расстояние ;от точки пересечения



dist_у;      // до позиции игрока

float xi,          // Используются при расчетах

yi;          // пересечений

// СЕКЦИЯ 1 ////////////////////////////////////////////

// ИНИЦИАЛИЗАЦИЯ

// Рассчитываем начальный угол по отношению к направлению

// взгляда игрока. Угол зрения - 60 градусов. Обрабатываем

//поочередно обе половины угла

if ( (view_angle-=ANGLE_30) < 0 }

{

view_angle=ANGLE_360 + view angle;

}

// цикл по всем 320 лучам

for(ray=319;ray>=0; ray--)

{

// СЕКЦИЯ 2 //////////////////////////////

// Вычислить первое пересечение по Х

// Нам необходимо узнать, какая именно половина плана,

// по отношению к оси Y, обрабатывается

if (view_angle >= ANGLE_0 && view_angle < ANGLE_180)

{

// Определяем первую горизонталь, которую может пересечь луч.

// На плане она должна быть выше игрока.

y_bound = (CELL_Y_SIZE + (у & 0xFFC0));

// Рассчитываем, смещение для перехода к следующей горизонтали

y_delta =- CELL_Y_SIZE;

// На основании первой возможной линии горизонтального

// пересечения рассчитываем отсечение по горизонтали

xi -= inv_tan_table[view_angle] * (y_bound - у) + х;

// Устанавливаем смещение ячейки

next_у_cell = 0;

} // Конец обработки else

{    

// Рассчитываем первую горизонталь, которую может пересечь луч.

// На плане она должна быть ниже игрока

y_bound = (int)(у & 0xFFC0);

// Рассчитываем смещение для перехода к следующей горизонтали

y_delta = -CELL_Y_SIZE;

// На основании первой возможной линии горизонтального

// пересечения рассчитываем отсечение по горизонтали

xi = inv_tan_table[view_angle] * (y_bound - у) + х;

// Устанавливаем смещение ячейки

next_у_cell = -1;

} // Конец обработки // СЕКЦИЯ 3 ////////////////////////////

// вычислить первое пересечение по Y

// Нам надо знать, которая именно половина плана,

// по отношению к оси X, обрабатывается

if (view_angle < ANGLE_90 || view_angle >= ANGLE_270)

{

// Рассчитываем первое отсечение по оси Y

// Определяем первую вертикаль, которую может пересечь луч.



// На плане она должна быть справа от игрока

x_bound = (int)(CELL_X_SIZE + (х & 0xFFC0));

// Определяем смещение для перехода к следующей ячейке

x_delta = CELL_X_SIZE;

// На основании первой возможной линии вертикального

// пересечения вычисляем первое отсечение по оси Y

yi = tan_table[view_angle] * (х_bound - х) + у;

// Устанавливаем смещение ячейки

next_x_cell = 0;

}

else

{

// Определяем первую вертикаль, которую может пересечь луч.

// На плане она должна быть левее игрока

x_bound = (int)(х & 0xFFC0);

// Определяем смещение для перехода к следующей ячейке

x_delta = -CELL_X_SIZE;

// На основании первой линии вертикального пересечения

// рассчитываем первое отсечение по оси Y

yi = tan_table[view_angle] * (x_bound - х) + у;

// Устанавливаем смещение ячейки

next_x_cell = -1;

} // Конец обработки

//начать отсечение

casting       = 2; // Одновременно обрабатываем 2 луча

хrау=уrау=0; // Сбрасываем флаги пересечения

// СЕКЦИЯ 4 /////////////////////////////

// Продолжаем расчет для обоих лучей

while(casting)

{

if (xray!=INTERSECTION_FOUND)

{ // Рассчитываем текущую позицию для проверки

сеll_х = ( (x_bound+next_x_cell) >> CELL_X_SIZE_FP);

cell_y = (int)yi;

cell_y>>=CELL_Y_SIZE__FP;

// Проверяем, не находится ли в проверяемой области блок

if ((x_hit_type = world[cell_y](cell_x])!=0)

{ // Рассчитываем расстояние

dist_x  = (long)((yi - у) * inv_sin_table[view_angle]) ;

yi_save = (int)yi;

xb_save = x_bound;

// Конец расчета по оси X

xray = INTERSECTION_FOUND;

casting--;

} // Конец обработки наличия блока

else

// Рассчитываем следующее пересечение по Y

{

yi += y_step[view_angle];

// Ищем следующую возможную Х-координату пересечения

x_bound += x_delta;

} // Конец else

}

// СЕКЦИЯ 5 //////////////////////////

if (уray !=INTERSECTION_FOUND)

{ // Рассчитываем текущую позицию для проверки

cell x = xi;

cell_x>>=CELL_X_SIZE_FP;

cell_y = ( (y_bound + next_y_cell) >> CELL_Y_SIZE_FP) ;

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



if ((y_hit_type = world[cell_y][cell_x]) !=0)

// Вычисляем расстояние

dist_y = (long)((xi - x) * inv_cos_table [view angle]);

xi_save = (int)xi;

yb_save = у_bound;

у_ray = INTERSECTION_FOUND;

casting--;

} // Конец обработки наличия блока else

{ // Прекращаем расчет по оси Y

xi += x_step[view angle];

// Вычисляем следующую возможную линию пересечения

у_bound += у_delta;

} // Конец else

}

} // Конец while

// СЕКЦИЯ 6 /////////////////////////////////

// На этом этапе мы вычислили точки пересечения с

// горизонтальными и вертикальными стенами. Теперь

// определяем, которая из них ближе

if (dist_x < dist_y)

{

// Вертикальная стена ближе

// Рассчитать масштаб и умножить его на поправочный

// коэффициент для устранения сферических искажений

scale = (int)(cos_table[ray]/dist_x);

// Отсечь фрагмент текстуры

if (scale>(MAX_SCALE-1)) scale=(MAX_SCALE-1);

scale_row = scale_table[scale-1];

if (scale>(WINDOW_HEIGHT-1))

{

sliver_clip = (scale-(WINDOW_HEIGHT-1)) >> 1;

scale=(WINDOW_HEIGHT-l) ;

}

else

sliver_clip =0;

sliver_scale = scale-1;

// Установить параметры для ассемблерной процедуры

sliver_texture = object.frames[x_hit_type];

sliver_column = (yi_save & 0х00ЗF);

sliver_top     = WINDOW_MIDDLE - (scale >> 1);.

sliver_ray     = ray;

// Отобразить фрагмент текстуры

Render_Sliver_32();

} // Конец if else

// горизонтальная стена ближе

{

//Рассчитать масштаб и умножить его на поправочный

//коэффициент для устранения сферических искажений

scale = (int)(cos_table[ray]/dist_y);

if (scale>(MAX_SCALE-l)) scale=(MAX_SCALE-1);

// Выполняем отсечение

scale_row      = scale_table{scale-l];

if (scale>(WINDOW_HEIGHT-1)) {

sliver_clip = (scale-(WINDOW_HEIGHT-1)) >> 1;

scale=(WINDOW_HEIGHT-l);

}

else

sliver_clip = 0;

sliver_scale = scale-1;

// Устанавливаем параметры для ассемблерной процедуры

sliver_texture= object.frames[y_hit_type+l];

sliver_column = (xi_save & 0x003F);

sliver_top     = WINDOW_MIDDLE - (scale >> 1) ;



sliver_ray     = ray;

// Отображаем текстуру

Render_Sliver_32();

} // Конец else

// секция 7 ///////////////////////////////////

// Переходим к следующему лучу

// Проверяем, не превысил ли угол 360 градусов

if (++view_angle>=ANGLE_360)

{

view_angle=0;

} // Конец if

} // Конец for

} // Конец функции

Новый отсекатель лучей сродни первому (файл RAY.С из шестой главы) и имеет то же самое строение. Оптимизация была проведена, скорее, на микроскопическом, нежели на макроскопическом уровне. Это означает, что я

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

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

Секция 1

Здесь мы не изменяли ничего.

Секция 2

Обратите внимание, как теперь вычисляется Y-пересечение: для этого используются целочисленные и логические операции. Основная идея заключена в том, что любое число, деленное нацело по модулю N, равно тому же самому числу, логически объединенному с М-1 по принципу AND. Другими словами,

Х % 64 == Х AND 63

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



Секция 3

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

Секция 4

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

Секция 5

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

Секция 6

Эта секция изменена совсем чуть-чуть:

§          Во-первых, кардинально изменено вычисление scale. Я провожу предварительное умножение масштаба изображения и разрешения совместно, что приводит к получению одного массива. Это экономит одну операцию умножения;

§          Во-вторых, я перекомпоновал текстурные элементы и уменьшил их размер;

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

Отображение текстур - это ассемблерная процедура, получающая входные данные через глобальные переменные программы. Это было сделано, для скорости. Текстурирование - чрезвычайно простая процедура. Когда обнаружено пересечение, точка вместе с соответствующим ей блоком 64х64 пикселя вычисляется и передается текстуре. Эта позиция (0-63) используется как индекс памяти текстуры для получения требуемого фрагмента.

Секция 7



Ничего интересного тут нет.

Я вполне доволен новым отсекателем лучей. Он работает отлично для такой простой программы, особенно, учитывая, что разработана она была всего за три дня! Это наглядно показывает, чего можно достичь, имея хотя бы минимум предусмотрительности и планирования.

Изображение текстуры

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



Эти текстуры размером 64х64 пикселя были созданы с помощью редактора DPaint. Как мы узнали ранее, изображение текстуры в игре с отсечением лучей. составляется из множества вертикальных полосок. Текстура масштабируется путем преобразования исходного количества пикселей изображения к окончательному размеру. Основная задача состоит в том, чтобы выполнить эту операцию быстро. Для этого, я предложил написать программу отрисовки фрагмента на ассемблере. Это функция, рисующая отмасштабированные и текстурированные вертикальные полоски. Она работает по тому же самому алгоритму, что мы обсуждали ранее. Только сейчас я предварительно вычислил индексы масштабирования- В сущности, я заранее рассчитал все возможные масштабы для размеров стен от 1 до 220 пикселей и, подсчитав индексы масштабирования, свел их в массивную таблицу (примерно на 40К). Следовательно, если размер изображения должен составлять 100 пикселей, вы обращаетесь к сотой строке справочной таблицы. Каждая точка берется из исходной матрицы изображения, используя массив данных сотой строки и n-го столбца, где n — рисуемый пиксель. Вероятно, я объясняю так, что кажется, будто это очень трудно сделать.


Жаль. Скорее всего, программа окажется более легкой для понимания! Листинг 19.2 содержит Сн-версию программы рисования фрагмента, а Листинг 19.3 содержит ее ассемблерную версию. Си-версия нуждается в добавлении механизма отсечения по границам игрового окна, но не беспокойтесь об этом. Только взгляните на внутренний цикл Си-версии и сравните его с ассемблерной версией.

Листинг 19.2. Си-версия программы для рисования фрагмента тек-стуры.

Render Sliver(sprite_ptr sprite,int scale, int column)

{

// Это обновленная версия функции рисования фрагмента текстуры.

// Она использует справочные таблицы с предварительно рассчитанными

// индексами масштабирования. В конце концов, я переписал ее

// на ассемблере из соображений скорости работы.

char far *work_sprite;

int far *row;

int work offset=0,offset,y,scale_off;

unsigned char data;

// Устанавливаем указатель на соответствующую строку

// справочной таблицы row = scale_table[scale];

if (scale>(WINDOW_HEIGHT-1))

{ scale_off = (scale-(WINDOW_HEIGHT-1))>>1;

scale=(WINDOW_HEIGHT-l);

sprite->y = 0;

}

// Устанавливаем указатель на спрайт для облегчения доступа

work_sprite = sprite->frames[sprite->curr_frame];

// Вычисляем смещение спрайта в видеобуфере

offset = (sprite->y << 8) + (sprite->y <<6) + sprite->x;

for(y=0;y<scale; y++)

{

double_buffer[offset] = work_sprite[work_offset+column];

offset  += SCREEN_WIDTH;

work_offset = row[y+scale_off];

} // Конец цикла for

) // Конец функции

Листинг 19.3. Ассемблерная версия программы рисования фрагмента текстуры (SLIVER.ASM).

; Эта функция является ассемблерной версией

; аналогичной функции на Си

; Она использует заранее вычисленные таблицы

; для увеличения скорости работы

;////////////////////////////

.MODEL MEDIUM, С             ; Используем модель MEDIUM

.CODE                       ; Начало сегмента кода

EXTRN double_buffer;DWORD   ; Буфер в оперативной памяти

EXTRN sliver_texture:DWORD  ; Указатель на текстуру



EXTRN sliver_column:WORD    ; Текущий столбец текстуры

EXTRN sliver_top:WORD       ; Начальная вертикальная позиция

EXTRN sliver_scale:WORD     ; Общая высота текстуры

ЕХТERN sliver_ray:WORD       ; Текущий столбец экрана

EXTRN sliver_clip:WORD      ; Какая часть текстуры отсекается?

EXTRN scale_row;WORD        ; Номер колонки в таблице ; масштабирования

PUBLIC Render_Sliver_32     ; Объявляем функцию общедоступной

Render_Sliver_32 PROC FAR С ; функция на Си

.386           ; использовать инструкции 80386 процессора

push si                ; сохранить регистры push di

les di, doubie_buffer  ; установить в ES:DI адрес буфера

mov dx,sliver_column   ; загрузить номер строки в DX

lfs si, sliver_texture ; FS:SI указывает на текстуру

; offset = (sprite->y « 8) + (sprite->y « 6) + sprite->x

mov bx,sliver_top      ; умножить Y на 320

; для вычисления смещения

shl bx,8

mov ax,bx

shr bx, 2

add bx,ax

add bx,sliver_ray      ; добавить Х

add di,bx

mov bx,sliver_clip       ; занести константы в регистры

mov ax,sliver_scale

add ax,bx

Sliver_Loop:                     ; основной цикл

; double_buffer [offset] = work_sprite [work_offset+column]

xchg dx,bx ; обменять содержимое BX и DX, так как

; только BX можно использовать в качестве

; индексного регистра

mov cl, BYTE PTR fs:[si+bx] ; получить пиксель текстуры

mov es:[di], cl ;поместить его на экран

xchg dx,bx      ; восстановить регистры ВХ и DX

mov сх,Ьх       ;готовимся работать с таблицей

; row = scale_table[scale]

mov dx, scale_row

shl bx,1

add bx, dx 

mov dx, WORD PTR [bx] ; выбираем масштаб из массива

add dx,sliver_column

mov bx,cx

; offset      += SCREEN_WIDTH;

add di,320            ; переходим к следующей строке

inc bx                ; инкремент счетчика

cmp bx, ax            ; работа закончена?

jne Sliver_Loop

pop di                ; восстанавливаем регистры

pop si

ret                   ; конец работы

Render_Sliver_32 ENDP END

В принципе, ассемблерный вариант программы делает то же самое, что и Си-версия.


Только в ассемблере используются глобальные переменные, выполняется небольшое отсечение и все работает быстрее. Я был вынужден использовать команды 386-го процессора и регистры дополнительного сегмента, чтобы ускорить работу программы, насколько возможно. Без использования регистров добавочного сегмента я был бы вынужден работать со стеком что немного уменьшило бы скорость выполнения. Теперь я должен извиниться за то, что сказал несколько раньше. Я говорил, что мы должны использовать 386 и 486 процессоры, поскольку они быстрее 8086. Это не было абсолютной правдой. Ведь применять команды, использующие 32-битовые регистры и все прочее в этом роде, можно только с привлечением ассемблера (конечно, если вы не используете DOS-расширитель вместе с 32-битным компилятором). Хотя, опять же, было бы лучше оставить эти части программы простыми и понятными.

Оттенение

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

§          Для вертикальных стен я использую одну версию текстуры;

§          Для горизонтальных стен я применяю просветленное изображение текстуры.

Изначально в программе было оттенение, но это уменьшило скорость ее работы примерно на 5 процентов. Но не это волновало меня больше всего. Основные проблемы, связанные с используемой техникой оттенения, возникли с палитрой. Я должен был разбить ее на части внутри зон одного и того же цвета с различными оттенками, но решил, что это было бы чересчур сложно для вас. Если вы хотите иметь оттенение, вы можете создать свою собственную палитру и использовать технические приемы, описанные в шестой главе, «Третье измерение», чтобы выбрать надлежащий оттенок в зависимости от таких параметров, как угол и расстояние.

Использование ассемблера

Использование ассемблера только иногда является последним средством увеличения быстродействия некоторой части программы до приемлемого уровня.


В Warlock я написал около 100 ассемблерных строк для оптимизации только наиболее критичных по времени выполнения кусков игры. Они, конечно, относились к визуализации графики. Существует определенная норма среди программистов игр для ПК: в основном игра пишется на Си, а затем некоторые функции, практически только относящиеся к графике, переписываются па ассемблере. (Если вы вдруг обнаружили, что, применяя ассемблер для реализации искусственного интеллекта или игровой логики, с вашей программой происходит что-то неладное, стоит хорошенько подумать, не вернуться ли к Си.) Я почти уверен, что лучшие мировые программисты могли бы сделать вашу и мою программы на Си значительно быстрее и без использования ассемблера. Запомните это, и когда найдете ассемблер пригодным не только для функций, связанных с графикой и низкоуровневым программированием, признайте, что надо сделать шаг назад (и шаг вперед... словно мы танцуем ча-ча-ча!) и начать сначала. Так или иначе, на ассемблере я переписал только две функции, которые перемещают содержимое дублирующего буфера в видеопамять и рисуют небо и землю (Листинг 19.5). Если уж на то пошло, их ассемблерный вариант занимает всего 5-10 строк.

Листинг 19.4. Функция, переносящая содержимое дублирующего бу-фера в видеопамять (DRAWG.ASM).

; Функция просто копирует дублирующий буфер в видеопамять

;Она использует 32-битовые операции и регистры

;для увеличения быстродействия

;/////////////////////

.MODEL MEDIUM,С               ; используем medium модель

; и соглашения языка Си

.CODE                         ;начало кодового сегмента

EXTRN double_buffer:DWORD     ; указатель на дублирующий буфер

PUBLIC Draw_Ground_32         ; делаем функцию общедоступной

Draw_Ground_32 PROC FAR С     ; функция поддерживает-соглашения

; по вызову для языка Си и является ; дальней

.386            ; использовать инструкции процессора 80386

push di                       ; сохраняем регистр DI

cld                           ; сбрасываем флаг направления



les di, double_buffer         ; загружаем в ES:DI адрес буфера

хоr еах,еах                   ; обнуляем регистр ЕАХ

mov сх,320*76/4               ; заполняем 76 строк

rep stosd                     ; делаем это

mov еах,01Е1Е1Е1Еh            ; загружаем в ЕАХ код серого цвета

mov сх,320*76/4               ; нам надо заполнить 76 строк

rep stosd                     ; делаем это

pop di                        ; восстанавливаем DI

ret                          ; конец

Draw_Ground_32 ENDP END

Листинг 19.5. функция визуализации неба и земли (RENDERB.ASM).

; Эта функция рисует землю и небо. В ней используются

;  32-битовые регистры и инструкции.

;//////////////////////////////////

.MODEL MEDIUM,С              ; использовать medium модель и соглашение Си

.CODE                        ; начало кодового сегмента

EXTRN double_buffer:DWORD    ; адрес дублирующего буфера

EXTRN video_buffer:DWORD     ; адрес видеобуфера

PUBLIC Render_Buffer_32      ; делаем функцию общедоступной

Render_Buffer_32 PROC FAR С ; функция поддерживает соглашения

; Си и является дальней

.386                 ; использовать инструкции 80386 процессора

push ds                      ; сохранить регистр сегмента данных

cld                        ; сбрасываем флаг направления

lds si, double_buffer        ; в регистры источника (DS:SI)

; загружаем адрес буфера

les di, video_buffer         ; в регистры приемника (ES:DI)

;загружаем адрес видеопамяти

mov сх,320*152/4             ; определяем количество двойных слов

rep movsd                    ; копируем

pop ds                       ; восстановить регистр сегмента данных

ret                          ; сделано!

Render_Buffer_32 ENDP

END

Цикл игры

Как я сказал, Warlock едва ли является полноценной игрой. Фактически, в ней вы можете только погулять, обозревая окрестности. Я создал механизм работы с трехмерной графикой, а вы сделайте из него игру. Но несмотря ни на что, в моей разработке все же присутствует некоторый набросок сюжета, который мы и можем обсудить.          



Вся программа игры сосредоточена в исходном модуле WARLOCK.C. Я собрал все в одном модуле вместо того, чтобы разбивать программу на части, чтобы вам легче было ее понять и добавить то, что вы сочтете нужным.

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

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

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

3.       Если на предыдущем шаге определено нажатие клавиш перемещения, то вычисляются новые координаты персонажа. Если они допустимы, то все в порядке, в противном случае персонаж возвращается на первоначальную позицию.

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

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

6.       Ожидание вертикальной синхронизации экрана (описанной в седьмой главе).


Это придает ходу игры определенный ритм и позволяет свести к минимуму мерцание изображения при его обновлении.

7.       Построение изображения отсечением лучей.

8.       Возврат к шагу 2.

Игра будет пуста без сюжета и противников. Это те вещи, которые вам необходимо ввести в нее. Я допускаю, что в моей версии игры маловато спрайтов, Но если бы я увеличил их количество, программу пришлось бы еще больше оптимизировать, переписывая отдельные части на ассемблере. Мне же хотелось оставить ее простой и позволить вам доработать ее самостоятельно. У вас уже есть все необходимое, чтобы добавить спрайты в игровое пространство и я дам вам несколько советов (в разделе «размещение объектов в пространстве»), чтобы нацелить вас в нужном направлении. А пока поговорим о формировании игрового пространства.

Игровое пространство

Игровое пространство или игровое поле представлено двухмерной матрицей размером 64х64 элемента. Она содержит целые числа, которые представляют собой различные виды текстуры, отображаемые на стенах элемента. Warlock получает эти данные из файла RAYMAP.DAT.

Для создания игрового пространства Warlock можно воспользоваться программой WarEdit (которую мы создали в пятнадцатой главе, «Инструментальные средства»), но прежде потребуется ее слегка модифицировать. Опять же, я хочу, чтобы это сделали вы сами. WarEdit создает поле размером 200х200 элементов, а для Warlock требуется создать массив 64х64. Поэтому вам потребуется изменить размерность и целочисленные коды объектов,

На рисунке 19.4 показана матрица игрового пространства, в котором вы движетесь.

Как видите, игровое пространство составляется из чисел, имеющих следующее толкование:

1 - Использовать текстуру № 1 для этих блоков;

3 - Использовать текстуру № 2 для этих блоков;

5 - Использовать текстуру № 3 для этих блоков;

7 - Использовать текстуру № 4 для этих блоков.

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


На самом деле каждая текстура имеет два оттенка, то есть существуют текстурные пары: (1,2), (3,4), (5,6) и т. д. Изображения текстур находятся в файле WALLTEXT.PCX. Они последовательно считываются слева направо и сверху вниз.

В сущности, это все о представлении игрового пространства. Не так уж и много. В WarEdit'e, если вы помните, мы использовали значительно больше определений для описания различных элементов и их сущности. В Warlock мы видим, что многое из этой начинки не является в настоящий момент необходимым. Однако если вы модернизируете ядро игры, эта начинка вам пригодится.

Одна из интересных вещей насчет Warlock заключена в демонстрационном режиме. Обсудим его механику.



Режим демонстрации

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

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

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

Чтобы выйти из режима демонстрации, просто нажмите клавишу Esc.


Если вы хотите создать спой собственный демонстрационный клип, вы можете раскомментировать определение MAKING_DEMO и изменить значение переменной demo_mode, присвоив ей значение 0. Это позволит создать демонстрационный клип под названием DEMO.DAT. Затем вы должны отменить все изменения, откомпилировать программу заново и запустить ее: демонстрация будет вашей собственной. Однако будьте осторожны! В программе отсутствует проверка на переполнение памяти и она будет серьезно повреждена, если вы превысите длину демонстрационного ролика (которая рассчитана примерно на 1000 команд).

Размещение объектов в пространстве

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

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

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

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


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

Быстрее, быстрее, еще быстрее

Перечислю некоторые вещи, которые можно сделать для ускорения трассировки

лучей.

§          Во-первых, начальная часть (секции 2 и 3) могут быть ускорены примерно на один процент за счет проведения некоторых предварительных вычислений;

§          Во-вторых, можно убрать последнюю оставшуюся операцию с плавающей точкой;

§          В-третьих, можно разделить трассировку по осям Х и Y по разным секциям, что сэкономит несколько операций сравнения;

§          В-четвертых, можно чередовать трассировку лучей и отрисовку экрана, трассируя лучи во время ожидания регенерации экрана;

§          Наконец, вместо режима 13h можно использовать так называемый режим X, имеющий разрешение 320x240 и являющийся наиболее быстрым из всех.

Но, это уже совсем другая история...

Несколько слов напоследок

Все, я больше не хочу говорить об играх для ПК. Теперь у вас есть необходимая техническая информация. Все остальное зависит от вашего воображения. Надеюсь, вы сможете написать совершенно невероятную игру, которая доставит другим удовольствие. Лично я делаю игры именно по этой причине. Но какие бы мотивы ни руководили вами, я надеюсь, что в вашем лице увижу новых исследователей виртуальных миров. И если мы встретимся где-нибудь в Туманности Андромеды, вам лучше держать ухо востро!

А может быть ваши путешествия будут магическими...

Любые вопросы, замечания и т.д. присылайте по адресу:

Andromeda Industries

P.O. Box 641744

San Jose, CA 95164-1744


Содержание раздела