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

         

Коротко! (То есть итог)


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



Кто пишет видеоигры?


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

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



Линии


Линия, как вы знаете, — это кратчайший отрезок между двумя точками. Например, между точками (1,1) и (5,5) на плоскости линия будет выглядеть так (рис. 4.2):

Давайте изменим программу из Листинга 4.1 так, чтобы она рисовала линии вместо точек. Чтобы сделать это, нужно кое-что изменить. Вместо двух случай­ных чисел х и у теперь их будет четыре: (х1,у1) и (х2,у2). Потом программа нарисует между ними линию, используя вызов библиотечной функции. Листинг будет выглядеть так:

 

 

 

 

 

 

 

Листинг 4.2. Программа, рисующая линии (LINER.С).

#include <stdio.h>



#include <graph.h>

void main(void)

{

int xl,yl,x2,у2,color,index;

// перевести компьютер в графический режим

_setvideomode(_VRES16COLOR); // режим 640х480, 16 цветов

// нарисуем 1000 случайных линий на экране

for (index = 0; index<1000; index++)

{

// получим случайные координаты концов линий и цвет

x1 = rand()%640; // Х-координата начальной точки

y1 = rand()%480; // Y-координата начальной точки

х2 = rand()%640; // Х-координата конечной точки

у2 = rand()%480; // У-координата конечной точки

color = rand()%16;

_setcolor(color);    // установить

цвет

_moveto(х1,у1);    // переместиться к началу линии

_lineto(х2,у2);      // нарисовать линию

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

// ждать нажатия любой клавиши

while(!kbhit()){}

// перевести компьютер в текстовый режим

_setvideomode(_DEFAULTMODE);

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



Локальное освещение


Локальное освещение представляет собой концентрацию интенсивности света в определенном направлении с целью освещения ограниченной части помещения. Например, если, лежа в кровати, вы включите фонарик и направите его на стену, то увидите световое пятно (рис. 6.33).  Если же вы включите свет, то, даже несмотря на рассеянное освещение, увидите, что область, освещаемая фонариком, сохранится до тех пор, пока интенсивность локального и рассеянного освещения не станут равны.



Локальные переменные


Теперь вы знаете, как передавать переменные в процедуры, а вот как насчет временных и локальных переменных, которые действуют только внутри процедуры?

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

Давайте посмотрим, как это делается. Когда мы определяем стек, сохраняя регистр ВР, то можем извлекать параметры путем прибавления положительного смещения к регистру ВР, например, [ВР+6] и т. д. Таким образом, получается, что в действительности стек — это область непрерывной памяти, которую мы можем использовать также и для хранения локальных переменных. Для этого нам надо только использовать отрицательное смещение относительно регистра ВР.

В случае, если мы хотим иметь две локальные переменные 1осаl_1 и 1оса1_2, можно использовать следующую подстановку:

local_1 EQU [ВР-2]

local_2 EQU [BP-4]

Это дает нам два целых числа. Пока нам не известно, что записано по этим адресам и мы можем только предполагать, что данный участок памяти можно использовать безболезненно. Однако нужно помнить, что мы только что использовали стек для хранения данных и теперь нам необходимо самим изменить регистр SP для отражения этого. Если этого не сделать, то первая встретившаяся инструкция PUSH обязательно что-нибудь запишет в нашу переменную и непременно «испортит» ее.

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

Листинг 2.4. Корректировка регистра SP.

push ВР         ; устанавливаем фрейм стека, как обычно

mov BP,SP

sub SP,4        ; корректируем указатель стека. Теперь наши

;переменные не будут случайно изменены

;Вся работа выполняется здесь

add SP,4        ; перед уничтожением фрейма стека надо восстановить

; исходное значение указателя стека

pop ВР

; уничтожаем фрейм стека,

; восстанавливая регистр ВР



Масштабирование изображения


Существует пара моментов, связанных с изменением масштаба изображений, используемых в качестве «кирпичиков», о которых необходимо упомянуть.

Поговорим о создании сцены дальнего плана в программе создания пейзажей наподобие Vista Pro. Предположим, вам нужно изменить масштаб полученного изображения так, чтобы можно было использовать нарисованные горы в качестве «кирпичиков» пейзажа. Помните, что изначально ваше изображение имеет размер 640х480 точек. В верхней части картинки (рис. 16.14) показан кусок неба, а в нижней - трава. Нас же в основном интересует центральная область, занятая горами и холмами.

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

Если вы хотите, чтобы окончательное изображение размещалось в двух кирпичиках 64х64, вы должны подстричь исходную картинку до 640х320 точек (чтобы ширина оказалась ровно в два раза больше высоты). Для трех кирпичиков нужно вырезать область размером 640х213 точек (высота равна 1/3 ширины), а для четырех - 640х160 (высота равна 1/4 ширины).

Существует еще несколько возможностей улучшить общее впечатление. Когда будете вырезать изображение, не устанавливайте высоту сразу пикселей, а захватите кусочек побольше, потому как вам может захотет сохранить немного неба или травы. Оставляйте немного дополнительных деталей, чтобы поиграть с ними во время предварительного отсечения, потому если из изображения удалить траву и небо, оно будет выглядеть со по-другому. На рисунке 16.15 отмечена область, которая в нашем примере будет удалена, а рисунок 16.16 показывает окончательное изображение размер 640х160 точек.

Следующим шагом нужно уменьшить изображение с 640х160 до 256х64 (до размера четырех "кирпичиков", размещенных один за другим). Многие графические программы позволяют масштабировать изображения. Некоторые из них не разрешают непосредственно указывать новую ширину и высоту изображения или  задавать масштаб в процентном соотношении. Чтобы определить эту велечину, разделите ширину изображения, которую нужно получить (в нашем случае 256)     на исходную ширину (640) и умножьте на 100. В нашем примере процентное соотношение размеров оригинального и результирующего изображении получается равным 40.



Масштабирование экрана


Поскольку в режиме 13h экран имеет размеры 320х200 точек, то возникает вопрос: «Как мы сможем увидеть вселенную размером 1000х1000 точек?» Есть много способов решения этой проблемы. Один из путей - это использование перспективной проекции. Когда мы удаляемся от объекта, он становится меньше. В некоторой точке вселенная размером 1000х1000 уменьшается настолько, что «влезает» в экран.

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

Чтобы нарисовать точку (х,у) на экране, где каждый из компонентов имеет диапазон измерения от 0 до 1000, мы производим преобразования

x_screen = 320 * х

/ 1000;

y_screen = 200 * у

/1000;

где х_screen и y_screen будут истинными координатами пикселя на экране размером 320х200.

Конечно, изменив таким образом масштаб экрана, вы не получите разрешения 1000х1000 точек. Разрешающая способность останется прежней - 320х200. Более того, некоторые точки будут изображены в одних и тех же местах. Например, точки с координатами 999 и 1000 сольются в одну, и вы не сможете их различить. Но это и не столь важно, поскольку трехмерное изображение на основе многоугольников отличается от битовых изображений двухмерного мира.



Масштабирование объектов


Масштабирование означает изменение размера объекта. Посмотрим для примера на рисунок 4.8. Астероид на нем в два раза больше, чем на рисунке 4.7. Это во многом объясняет принципы масштабирования. Все, что нам надо сделать, это умножить координаты каждой из вершин объекта на коэффициент масштабирования. Фрагмент кода в Листинге 4.6 показывает, как это делается для структуры объекта, определенной нами ранее.

Листинг 4.6. Масштабирование астероида.

void Scale_Object(object_ptr object,float scale)

{

int index;

// для всех вершин масштабируем х- и у-компоненты

for (index = 0; index<object->num vertices; index++)

{

object->vertices [index].х *= scale;

object->vertlces[index].y *= scale;

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

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

Функция из Листинга 4.6 работает путем масштабирования координат каждой из вершин объекта. Если нам придет в голову увеличить наш объект «астероид»- в два раза, то нам потребуется написать следующее:

Scale_Object((object_ptr)&asteroid,2, 0};

С этим, вроде, все. Теперь мы уже готовы приступить к вращению объекта.



Масштабирование растровых изображений


Люди обычно говорят: «А вот это мы оставим на сладкое...». В нашем случае мы оставили на сладкое самое сложное, что есть в этой главе. Я пытался свести всю математическую часть к минимуму, зная «любовь» программистов к тригонометрическим функциям и умножению. К сожалению, трехмерные игры содержат довольно сложные алгоритмы и математику. Масштабирование битовых образов - это именно одна из таких областей.

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

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

Например, если бы вы захотели увеличить ковбоя с рисунка 7.9 в два раза, то он должен выглядеть так, как это показано на рисунке 7.10. 

Если вы посмотрите внимательно, то увидите, что на рисунке 7.10 ровно в два раза больше пикселей, чем на рисунке 7.9.

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

Давайте подойдем к решению данной проблемы немного с другой стороны. На время забудем об умножении на 0.5 или 2, а подумаем о растягивании и сжатии исходного образа. Другими словами, мы хотим взять исходное количество пикселей и растянуть или сжать их до требуемого количества пикселей. Рисунок 7.11 показывает графическое представление этой идеи.






Например, если мой исходный образ состоит из строк по 64 пикселя, и я хочу превратить его в образ со строкой по 100 пикселей, то я должен увеличить количество исходных пикселей как 100/64, то есть примерно в 1.5 раза.

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

Вернемся к нашему примеру. Мы хотим в результате масштабирования получить 100 пикселей, которые бы выглядели как исходные 64. Как мы уже посчитали, коэффициент масштабирования должен быть 1.5, следовательно, каждый пиксель должен быть скопирован 1.5 раза. Однако мы не можем этого сделать, так как скопировать только половину пикселя при всем желании не удастся. Испробуем наш альтернативный вариант, и сделаем примерно так:

§

Проиндексируем область значений исходного изображения от 0 до 63;

§          Проиндексируем область значений увеличенного объекта также от 0 до 63, но не целыми, а дробными значениями.

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

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

1/коэффициент масштабирования

И все! Конечно, мы должны изменять масштаб и по координате X, и по координате Y, но алгоритм можно использовать один и тот же. Более того, если объект имеет одинаковые размеры по координатам Х и Y (то есть если он имеет размеры МхМ, а не MxN), то расчеты могут быть выполнены еще быстрее.



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

Чтобы показать работу функции масштабирования объектов я написал демонстрационную программу, которая называется SCALE.С. В ней вы можете выбрать одну из четырех текстур и масштабировать ее, используя клавишу левой угловой скобки (или знака «меньше») для уменьшения и клавишу правой угловой скобки (или знака «больше») — для увеличения объекта. Обратите внимание, что масштабирование действительно замедляется при увеличении объекта, но надо помнить, что это только начало, и нам было важнее всего понять, как все это работает. Листинг 7.10 содержит текст программы. Не забудьте при компоновке программы подключить библиотеку GRAPH0.C.

Листинг 7.10. Программа, масштабирующая текстуры стен (SCALE.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" // включаем нашу графическую библиотеку

// ГЛОБАЛЬНЫЕ ПЕРЕМЕННЫЕ //////////////////////////////////

sprite object;

pcx_picture text_cells;

// ФУНКЦИИ ///////////////////////////////////////////////

 void Scale_Sprite(sprite_ptr sprite,float scale)

{

// эта функция масштабирует спрайт, рассчитывая количество

// дублирования исходных пикселей, необходимое для получения

// требуемого

размера

char far *work_sprite;

int work offset=0, offset,x,у;

unsigned char data;

float y_scale_index,x_scale_step,y_scale_step,x_scale_index;

// берем первый пиксель исходного изображения



y_scale_index = 0;

// рассчитываем

дробный шаг

y_scale_step = sprite height/scale;

x_scale_step = sprite_width/scale;

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

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

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

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

// построчно масштабируем спрайт

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

{

// копируем следующую строчку в буфер экрана

х_scale_index=0;

for (х=0; x<(int)scale; х++)

{

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

// (то есть равен ли он 0), если нет - прорисовываем пиксель

if ((data=work_sprite[work_offset+(int)x_scale_index]))

double_buffer[offset+x] = data;

x_scale_index+=(x_scale_step);

} // конец внутреннего цикла (по X)

// используя дробный шаг приращения, определяем следующий

// пиксель исходного изображения

у_scale_index+=y_scale_step;

// переходим к следующей строке видеобуфера

//и растрового буфера спрайта

offset      += SCREEN_WIDTH;

work_offset = sprite_width*(int)(y_scale_index);

} // конец внешнего цикла (по У)

} // end Scale_Sprite /////////////////////////////////////////

void Clear_Double_Buffer(void)

// эта функция очищает дублирующий буфер

// несколько грубо, зато работает

_fmemset(double_buffer, 0, SCREEN_WIDTH * SCREEN_HEIGHT +1);

} // конец Clear_Double_Buffer

// ОСНОВНАЯ ПРОГРАММА /////////////////////////////////////

void main(void)

{

int index, done=0 ;

float scale=64;

// установка видеорежима 320х200х256

Set_Mode(VGA256);

// установка размера спрайта

sprite_width

= sprite_height = 64 ;

// инициализация файла PCX, который содержит

// мультииликационные кадры

PCX__Init ((pcx_picture_ptr)&text_cells) ;

// загрузка файла PCX, который содержит мультипликационные кадры

PCX_Load("textures.pcx", (pcx_picture_ptr)&text_cells,1) ;

// PCX_Show_Buffer((pcx_picture_ptr)&text_cells);

Sprite_Init ( (sprite_ptr) &object, 0, 0, 0, 0 ,0, 0);

// выборка четырех интересующих нас текстур



PCX_Grap_Bitmap((pcx_picture_ptr)&text_cells,

(sprite_ptr)&object,0,0,0);

PCX_Grap_Bitmap((pcx_picture_ptr)&text_cells,

(sprite_ptr)&object,1,1,0);

PCX_Grap_Bitmap((pcx_picture_ptr)&text cells,

(sprite_ptr)&object,2,2,0) ;

PCX_Grap_Bitmap((pcx_picture_ptr)&text_cells,

(sprite_ptr)&object,3,3,0) ;

// резервируем память под дублирующий буфер

Init_Double_Buffer() ;

// позиционируем объект в центре экрана

object.curr frame =0;

object.х          = 160-(sprite_width>>1);

object.у          = 100-(sprite height>>1);

// очищаем дублирующий буфер

Clear_Double_Buffer();

// воспроизводим масштабированную текстуру

Scale_Sprite((sprite_ptr)&object,scale) ;

Show_Double_Buffer(double buffer) ;

_settextposition(24,0);

Printf("Q - Quit, < > - Scale, Space - Toggle.");

// главный цикл while(!done)

{

// нажал ли игрок клавишу?

if (kbhit())

{

switch(getch()) {

case '.': // увеличиваем объект

{

if (scale<180) ( scale+=4;

object.x-=2;

object.y-=2;

} // конец оператора if

} break;

case ',' : // уменьшаем объект

{

if (scale>4) {

scale-=4;

object.x+=2;

object.y+=2;

} // конец оператора if

} break;

case ' ': // смена текстуры

{

// текстуры исчерпались?

if (++object.curr_frame==4)

object. curr_frame=0 ;

} break;

case 'q': // до свидания! {

done=1;

) break;

default:break;

} // конец оператора switch

// очистка дублирующего буфера

Clear_Double_Buffer() ;

// масштабирование спрайта и прорисовка его

//в дублирующем

буфере

Scale_Sprite((sprite_ptr)&object,scale);

// вывод дублирующего буфера на экран

Show_Double_Buffer(double_buffer);

_settextpostion(24,0) ;

printf("Q - Quit,"< > - Scale, Space - Toggle.");

}// конец оператора,if

} // конец оператора..while

// удаление

файла PCX

PCX_Delete((pcx_picture_ptr)&text_cells);

// возврат в текстовый режим

Set_Mode(TEXT_MODE);

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


Масштабирование трехмерного объекта


Следующая операция трансформации, которую должны уметь выполнять, это масштабирование. Изменение размеров трехмерного объекта похоже на двухмерное масштабирование. Здесь показано масштабирование точки (x,y,z) с коэффициентом S:

х=х * S;

у=у * S;

z=z * S;

Все очень просто. Только кажется, что трехмерная графика сложна для понимания. Для описания преобразований с помощью матриц, мы опять должны представить точку в виде (x,y,z,1):

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

Это приведет к неоднородному масштабированию.



Математические основы отсечения лучей


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

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

§

рисование лучей;

§          вычисление координат первого пересечения;

§          вычисление координат следующего пересечения;

§          вычисление.расстояния;         

§          масштабирование;

§          уменьшение проекционного искажения;

§          рисование полос.



Математические основы параллельных проекций


Математика, необходимая для произведения параллельной проекции, элемен­тарна - вы просто убираете z-координату и рисуете каждую точку объекта по координатам (х,у). Вот и вся трансформация. Для примера возьмем точку (x,y,z).

x_parallel = х

y_parallel= у

plot х,у



Математические основы верспективной проекции


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

Рассмотрим эту операцию на примере: дана точка (x,y,z), удаленная от плана просмотра на расстояние D:

x_perspective = D * х

/ z

y_perspective = D * у

/ z

Вот и все. Достаточно умножить каждый компонент на расстояние и разделить на значение Z-координаты. Образ получается похожим на трехмерный.



Матрицы


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

Матрица — это множество чисел, сгруппированных в колонки и столбцы. Здесь изображены две матрицы: Матрица А и Матрица В.

Матрица А — это матрица 2х3 (то есть у нее две строки и три столбца), тогда как матрица В — это матрица 3х3. Мы можем получить доступ к элементу матрицы А, используя запись А[m,n], где m - это строка, а n - столбец. Элемент в левом верхнем углу матрицы А будет обозначаться А[0,0] и он равен, единице.



Механика трехмерных спрайтов


Трехмерные спрайты очень похожи на двухмерные. По правде говоря, они и являются плоскими, просто нам кажется, что они объемные. Такое восприятие обуславливается тем, как они движутся по экрану и как при этом изменяется их размер- Двухмерные спрайты могут перемещаться только по осям Х и У, трехмерные же должны перемещаться ло всем трем осям — X, У и Z. Движение вдоль оси Z, фактически, осуществляется только в математическом плане за счет изменения координат спрайта одновременно по осям Х и Y, а также вычислением его размера, определяемого координатой Z. Именно это и делает плоские  картинки похожими на трехмерные.                          

Конечно, мы опустили некоторые детали. Как может плоское изображение выглядеть трехмерным? Чтобы ответить на этот вопрос, нарисуем объект так;  как бы он выглядел под определенным углом зрения в пространстве. В качестве

примера к этой главе, я заснял на видеокамеру вращение игрушечного космического корабля, а затем оцифровал полученные кадры. Для этого я закрепил камеру и объект на определенном расстоянии друг от друга, как показано на рисунке 8.1, и для каждого снимка чуть-чуть поворачивал игрушку вокруг вертикальной оси. Таким образом, я получил 12 кадров, которые изображали корабль под разными углами. Эти кадры приведены на рисунке 8.2. Если мы последовательно отобразим их на экране, то получим иллюзию вращающегося в сотне шагов от нас космического корабля.

Трехмерный вид спрайтов обуславливается двумя различными факторами:

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


Если мы хотим просто вращать объект относительно оси Y с шагом в 30 градусов и рассматривать его с 4 разных уровней высоты, на это уйдет 12х4х3840=184К. А теперь представим, что нам понадобились изображения большего размера либо несколько подобных изображений (или и то и другое сразу)! Для экономии памяти можно оцифровывать только некоторые из требуемых изображений, а недостающие получать преобразованием имеющихся по мере необходимости. Например, можно нарисовать виды объекта только под углами зрения в пределах от 0 до 180 градусов, а для оставшихся 180 градусов получать изображение из исходного набора данных уже программным путем;

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

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

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


Мнимое время, прерывания и многозадачность


Если вы когда-нибудь играли в компьютерные игры... впрочем, такое начало главы совершенно не подходит для этой книги! Короче говоря, вы наверняка подметили, что все в хороших играх выглядит так, как будто происходит одновременно. Но я уверен, что в 99,9 процентах случаев это не так! Компьютер просто-напросто делает все настолько быстро, что создается впечатление многозадачности. Эта глава как раз и посвящена примитивным возможностям персонального компьютера по поддержке многозадачности. Кроме того, мы научимся писать «цикл игры», который, собственно, и создаетвпечатление одновременности событий игры.Мы также обсудим некоторые вопросы архитектуры компьютерных игр, относящиеся кразработке и написанию «самодостаточных» функций. В этой главе будут рассмотрены следующие вопросы:

§          Восприятие игры;

§          Многозадачность;

§          Реализация многозадачности при помощи прерываний;

§          Создание обработчиков прерываний на Си;

§          Цикл игры;  

§          Автономные функции; 

§          Функции ответа;                       

§          Перепрограммирование системных часов;

§          Объединение компонентов многозадачности в единое целое;

§          Пример обработчика прерываний № 1 - Там полно звезд...

§          Пример обработчика прерывания № 2 - Ловим нажатия клавиш!



Многоугольники


Линии весьма просты, и если вы приложите немного усилий, то сможете из программы 4.2 сделать простой Screen Saver. Но видеоигры кроме линий содержат еще множество интересных графических объектов, например, многоугольников.

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

Все многоугольники являются закрытыми геометрическими объектами. Многоугольники бывают двух типов. Оба типа представлены на рисунке 4.4.

Существует математический алгоритм, позволяющий определить, к какому типу относится данный многоугольник, но он довольно сложен и сейчас нам не нужен. Давайте лучше напишем программу, рисующую многоугольники (Листинг 4.3).

 

 

 

 

Листинг 4.3. Программа, рисующая многоугольники (POLYDRAW.C).

#include <stdio.h>

#include <graph.h>

void main(void)

{

// перевести компьютер в графический режим

_setvideomode( VRES16COLOR); // режим 640х480, 16 цветов

// нарисовать простой многоугольник

_setcolor(1);        // пусть он будет синего цвета

_moveto(100,100); // первая вершина

_lineto(120,120);   // вторая вершина

_lineto(150,200);   // третья вершина

_lineto(SO,190);    // четвертая вершина

_lineto(90,60);     // пятая вершина

_lineto (100,100); // назад для замыкания контура

// теперь отмечаем каждую вершину белым цветом

_setcolor(15); // белый цвет

_setpixel(100,100); // вершина 1

_setpixel(120,120); // еершина 2

_setpixel(150,200); // вершина 3

_setpixel(80,190); // вершина 4

_setpixel(80,60); // вершина 5

// ожидание нажатия любой клавиши

while(!kbhit()){}

// перевести компьютер в текстовый режим

_setvideomode(_DEFAULTMODE);

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



Моделирование реального мира


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

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

 Object_Position += 2;

Если вы посмотрите на координаты объекта как на функцию времени, та увидите график, приведенный на рисунке 11.9.

Что такое ускорение? Ускорение - это изменение скорости за единицу времени или, другими словами, приращение скорости. Когда вы в своем автомобиле нажимаете на акселератор, то скорость становится все 6ольше ибольше. Ускорение может быть записано в следующем виде:

Object_Position += velocity;

velocity += acceleration_factor;

Приведенный фрагмент программы увеличивает скорость. Это выглядит как ускорение. Чаще всего мы сталкиваемся с ускорением свободного падения. Когда объект падает на землю, его ускорение составляет 9,8 м/сек . То есть если в первую секунду падения его скорость достигнет 9,8 м/сек, то во вторую она вырастет до 19,6 м/сек.

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

Листинг 11.5. Падение мяча (BALL.C).

// ВКЛЮЧАЕМЫЕ ФАЙЛЫ ///////////////////////////////

#include <stdio.h>

#include <math.h>

#include <graph.h>                                                 

//определения //////////////////////////

#define EARTH_GRAVITY 9.8

//ГЛОБАЛЬНЫЕ

ПЕРЕМЕННЫЕ




////////////////////////////

unsigned int far *clock = (unsigned int far *)0x0000046C;

                        // указатель на внутренний таймер

// функции ///////////////////////////////

void Timer(int clicks)

{

// эта функция использует внутренний таймер с частотой 18.2 "тик"/c.

// 32-битовое значение этого таймера находится по адресу 0000:046Ch

unsigned int now;

// получаем текущее время now

= *clock;

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

// каждый "тик" имеет длительность примерно в 55мс.

while(abs(*clock - now) < clicks){}

} // конец Timer

// ОСНОВНАЯ ПРОГРАММА ////////////////////////////////

void main(void)

{

float ball_x   = 160, ball_y = 50, ball_yv = 0, ball_acc = EARTH_GRAVITY;

int done=0, key;

// используем графические функции Си

_setvideomode(_MRES256COLOR) ;

_settextposition(0,0);

printf("Q to quit, use +,- to change gravity.");

while(!done)

{ // была ли нажата клавиша?

if

(kbhit())

{

// какая клавиша была нажата?

switch(getch())

{

case ' -' :

{

ball_acc-=.1;

} break;

case '=':

{

ball_acc+=.1;

} break;

case 'q' :

{

done=1;

} break;

} // конец оператора switch

// сообщим игроку новое значение ускорения

_settextposition(24,2);                  

printf("Gravitational Constant = %f",ball_асc);

} // конец оператора if

// стираем изображение мяча

_setcolor(0) ;

_ellipse(_GBORDER, ball_x,ball_y,ball_x+10,ball_y+10);

// перемещаем мяч

ball_y+=ball_yv;                             

// увеличиваем ускорение

ball_yv+=(ball_acc*.1);                      

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

if (ball_y>190)

{

ball_y=50;

ball_yv=0 ;

} // конец оператора if

// рисуем мяч

_setcolor(l) ;

_ellipse(_GBORDER, ball_x,ball_y,ball_x+10,ball_y+10) ;

// немного подождем

Timer(2);

} // конец while

// восстановить начальный видеорежим

_setvideomode(_DEFAULTMODE);

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

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



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



Таким образом, чтобы смоделировать это, мы должны только отразить значение скорости атома и все будет выглядеть «корректно с точки зрения физики». Текст программы приведен в Листинге 11.6.

Листинг 11.6. Идеальный газ (GAS.C).

// ВКЛЮЧАЕМЫЕ ФАЙЛЫ ////////////////////////////////////////

#include <dos.h>

#include <bios.h>

#include <stdio.h>

#inciude <math.h>

#include <conio.h>

#include <graph.h>                             

// ОПРЕДЕЛЕНИЯ /////////////////////////////////////////

#define NUM_ATOMS 300

// СТРУКТУРЫ ///////////////////////////////////////////////

// структура атома

typedef struct ant_typ

{             

int x,y;            // позиция атома

int xv,yv;        // скорость атома

} atom, *atom_ptr;

// ГЛОБАЛЬНЫЕ

ПЕРЕМЕННЫЕ' ///////////////////////////////////

unsigned char far *video_buffer = (char far *)0xA0000000L;

// указатель

на видеобуфер

unsigned int far *clock = (unsigned int far *)0x0000046C;.

// указатель на внутренний таймер

// наши

атомы

atom atoms[NUM_ATOMS];

// ФУНКЦИИ /////////////////////////////////////////////////

void Timer(int clicks)

{

// эта функция использует внутренний таймер с частотой 18.2 "тик"/с.

// 32-битовое значение этого таймера находится по адресу 0000:046Ch

unsigned int now;

// получить текущее время

now = *clock;

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


Заметьте, что

// каждый "тик" имеет длительность примерно в 55мс.

while(abs(*clock - now) < clicks){}

} // конец Timer

////////////////////////////////////////////////////////

void Plot_Pixel_Fast(int x,int y,unsigned char color)

{ // эта функция рисует точку заданного цвета несколько быстрее чем

//обычно, за счет применения операции сдвига вместо операции // умножения

// используется тот факт, что 320*у = 256*у + 64*у = у<<8 + у<<6

video_buffer[((y<<8) + (у<<6)} + х] = color;

} // конец Plot_Pixel_Fast ////////////////////////////////////////////

void Initialize_Atoms (void)

{

int index;

for (index=0; index<NUM_ATOMS; index++)

{

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

//и их фона

atoms[index].х     = 5 + rand()%300;

atoms [index] .у     = 20 + rand()%160;

atoms [index] .xv    = -5 + rand() %10;

atoms[index].yv    = -5 + rand()%10;

} // конец

цикла

} // конец Initialize_Atoms

////////////////////////////////////////////////

void Erase_Atoms(void)

{

int index;

// обрабатываем в цикле все атомы, стирая их изображения

for (index=0; index<NUM_ATOMS; index++)

{

Plot_Pixel_Fast( atoms[index].x, atoms[index].y, 0);

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

} // конец Erase_Atoms

////////////////////////////////////////////////////////////

void Move_Atoms(void)

{ int index;

// обрабатываем в цикле все атомы, перемещая каждый атом.

// Также проверяем столкновение атомов со стенками контейнера.

for (index=0; index<NUM_ATOMS; index++)

{

// переместить атомы

atoms[index].x+=atoms[index].xv;

atoms[index].y+=atoms[index].yv;

// если атом столкнулся со стенкой, меняем знак скорости

// на противоположный

if (atoms[index].х > 310 11 atoms[index].x <10)

{

atoms[index].xv =-atoms[index].xv;

atoms [index].x+=atoms [index].xv;

} // конец оператора if

if (atoms[index].у > 190 11 atoms[index].у <30)

(

atoms[index].yv=-atoms[index].yv;

atoms[index].y+=atoms[index].yv;

} // конец оператора if



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

} // конец Move_Atoms

////////////////////////////////////////

void Draw_Atoms(void)

{

int index;

// рисуем все атомы в цикле

for (index=0; index<NUM_ATOMS; index++)

{

Plot_Pixel_Fast (atoms[index] .x, atoms [index] .y, 10) ;

} // конец

цикла

} // конец Draw_Atoms

// ОСНОВНАЯ ПРОГРАММА //////////////////////////////////////

void main(void)

{

// устанавливаем режим 320х200х256

_setvideomode(_MRES256COLOR) ;

_settextposition[2,0);

printf("Hit any key to exit.");

// рисуем контейнер _setcolor(9);

_rectangle(_GBORDER,0,16,319,199);

// инициализируем атомы

Initialize_Atoms();

while(!kbhit())

{

// стираем все атомы

Erase_Atoms

() ;

// перемещаем все атомы

Move_Atoms();

// теперь рисуем атомы

Draw_Atoms();

// немного подождем

Timer(1);

} // конец while

// восстанавливаем исходный видеорежим

_setvideoroode(_DEFAULTMODE) ;

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

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


Модем


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

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



Мультипликация с помощью регистров цвета


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

Мультипликация с помощью регистров цвета основана на том факте, что образ — это набор цветов. Эти цвета, в свою очередь, определены значениями RGB в таблице выбора цвета. Например, представим, что мы имеем два идентичных объекта — два изображения дерева, каждое из которых состоит из пикселей восьми оттенков зеленого. Мы создаем два набора цветов, каждый из которых содержит одинаковые восемь оттенков. Затем, используя для каждого дерева свой набор, мы рисуем две картинки: то есть изображаем первое дерево, используя первый набор цветов, и второе — используя второй, как это показано на рисунке 7.5.

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

   На этом и основана мультипликация с использованием регистров цвета. Схематично это происходит так;

§

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

§          Затем делаем одно из изображений объекта невидимым, обнуляя соответствующие регистры цвета;

§          Потом заменяем регистры цвета «включенного» объекта значениями RGB «выключенного» изображения, оставляя другой банк регистров цвета обнуленным.       

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




Такую технику можно использовать для создания объектов, изменяющих свой внешний вид, движущихся по экрану или исчезающих. Чтобы показать вам, как это может работать, я создал демонстрационную программу BIRDANI.C, которая рисует маленькую птичку, летающую по кругу, но на самом деле неподвижную. Я нарисовал птичку в графическом редакторе, использовав при этом 13 оттенков серого цвета. Затем, с помощью программного обеспечения для работы с PCX, описанного в пятой главе, «Секреты VGA-карт», загрузил это изображение. После чего обнулил все цветовые регистры, составляющие эти 13 оттенков серого цвета. Затем я поместил серый в первый цветовой регистр, потом обнулил его, поместив серый в следующий цветовой регистр и так далее- В результате все это выглядит так, как будто птица взмахивает крыльями и летит по кругу. Листинг 7.6 содержит текст этой программы.

Замечание

Отныне в этой книге я буду использовать перекомпилированные библиотеки, поэтому вы больше не увидите старых функций в листингах программ. Текст большинства из этих функций я поместил в файлы с именем GRAPHO.C. Соответственно, файл заголовка будет называться GRAPHO.H. Чтобы функции GRAPH0.C были доступны для наших программ мы должны сначала скомпилировать сам GRAPH0.C (только убедитесь, что вы используете модель памяти MEDIUM). Затем во время липковки скомпонуйте получившийся объектный файл с той программой, с которой вы сейчас работаете. Не забудьте включить в вашу программу файл заголовка GRAPH0.H, иначе функции из GRAPH0.C будут иедоступны. Вы могли бы также создать библиотеку графических функций, используя программу Microsoft's Library Manager, LIB- Правда у нас только один файл с исходным текстом библиотечных функций, поэтому данная программа для нас излишне мощна. Однако решать, конечно же, вам. Короче говоря, если код из GRAPH0.C, GRAPH0.H и функция установки видеорежима каким-либо образом включены в программы этой главы, то они должны работать.

Листинг 7.6. Мультипликация с помощью изменения регистров цвета (BIRDANI.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 "graphl.h" // включаем нашу графическую библиотеку

// ОПРЕДЕЛЕНИЯ /////////////////////////////////////////////

#define BIRD_START_COLOR_REG 16

#define BIRD_END_COLOR_REG   28

// ГЛОБАЛЬНЫЕ ПЕРЕМЕННЫЕ ///////////////////////////////////

unsigned int far *clock = (unsigned int far *)Ox0000046C;

// указатель на внутренний таймер 18.2 "тик"/с

pcx_picture birds;

// ФУНКЦИИ /////////////////////////////////////////////////     

void Timer(int clicks)

{

// эта функция использует внутренний таймер с частотой 18.2 "тик"/с.

// 32-битовое значение этого таймера находится по адресу 0000:046Сh

unsigned int now;

// получаем текущее время

now = *clock;

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

// Заметьте, что каждый "тик" имеет длительность-примерно в 55 мс

while(abs(*clock - now) < clicks){}

} // конец Timer

////////////////////////////////////////////////////////////

void Animate_Birds(void)

{            

// эта функция перемещает птичку, изображенную 13-ю различными // цветами, последовательно включая один цвет и выключая остальные

RGB_color color_l, со1оr_2;

int index;

// очистка каждого из цветовых регистров,

// используемых в изображении птички

color_l.red   = 0;

color_l.green = 0;

color_l.blue  = 0;

color_2.red   = 0;

color_2.green = 63;

color_2.blue  = 0;

// очистка всех цветов

for (index=BIRD_START_COLOR_REG;

index<=BIRD_END_COLOR_REG; index++)

{

Set_Palette_Register(index, (RGB_color_ptr)&color_l);

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

// делаем первую птичку зеленой и затем

// последовательно меняем цвета

Set_Palette_Register(BIRD_START_COLOR_REG,



   (RGB_color_ptr)&color_2) ;

// мультипликация цветами

while(!kbhit())

{

// меняем цвета

Get_Palette_Register(BIRD_END_COLOR_REG,

(RGB_color_ptr)&color_l) ;

for (index=BIRD_END_COLOR_REG-l;

index>=BIRD_START_COLOR_REG; index—) {

Get_Palette_Register(index,(RGB_color_ptr)&color_2) ;

Set_Palette_Register(index+l,(RGB_color_ptr)&color_2);

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

Set_Palette_Register(BIRD_START_COLOR_REG,

         (RGB_color_ptr)&color__l);

// небольшая пауза

Timer(3);

) // конец оператора while

} // конец Animate_Birds

// ОСНОВНАЯ ПРОГРАММА //////////////////////////////////////

void main(void)

{

int index,done=0;

// установка видеорежима 320х200х256

Set_Mode(VGA256);

// инициализация PCX-файла, который содержит изображение птички PCX_lnit((pcx_picture_ptr)&birds) ;

// загрузка файла PCX

PCX__Load("birds.pcx", (pcx_picture_ptr)&birds,1); 

PCX_Show_Buffer((pcx_picture_ptr)&birds);

PCX_Delete((pcx_picture_ptr)&birds); 

_settextposition(0,0);

printf(" Hit any key to see animation."};

getch()

_settextposition(0,0) ;

print£("Hit any key to Exit.           ");

Animate_Birds();                           

// возврат в текстовый режим

Set_Mode(TEXT_MODE);

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

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


Музыка И MIDI


Так же, как и в случае с частотным синтезатором, у нас нет достаточного времени, чтобы разобраться во всех тонкостях воспроизведения музыкич с помощью Sound Blaster, Мы лишь затронем этот допрос для того, чтобы у вас сложилось общее представление об этом. Я хотел бы, по крайней, мере рассказать о том, что вам, возможно, пригодится в будущем. Sound Blaster поставляется вместе с драйвером для поддержки воспроизведения MIDI-музыки. (Если вы уже забыли, MIDI - это интерфейс электромузыкальных инструментов.) Этот драйвер похож на CT-VOICE.DRV, который мы обсуждали выше, и называется SBFMDRV.EXE. Функции этого драйвера приведены в таблице 9.3.

Табл. 9.3. Функции драйвера FMDRV.DRV для Sound Blaster

Функция

Описание

0

Получить версию драйвера

1

Установить адрес байта состояния

2

Установить таблицу инструментов

3

Установить частоту системного таймера

4

Установить частоту таймера драйвера

5

Транспонировать музыку

6

Начать воспроизведение музыки

7

Остановить воспроизведение музыки

8

Инициализировать драйвер

9

Приостановить воспроизведение музыки

10

Возобновить воспроизведение музыки

11

Задать пользовательское прерывание для системы

Это все. что я хотел сказать о музыкальном драйвере. Я могу поспорить, что мастеру в написании компьютерных игр этой информации вместе с теми программами, что мы написали для CT-VOICE.DRV, вполне достаточно.



Мышь


Изобретение мыши, безусловно, было событием в компьютерной истории. Появившись в результате исследований в центре Xerox PARK в Калифорнии, она завоевала сердца миллионов пользователей. Мышь позволила наглядно работать с экраном и легко общаться с интересующими объектами. Обычно мышь подсоединяется к последовательному порту компьютера или DIN коннектору на лаптопе. Существующая bus mice

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

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

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

Мы будем использовать только минимум функций для определения позиции мыши и статуса кнопок. В таблице 3.3 перечислены эти функции.

Замечание

Микки (mickey) — это самое маленькое расстояние, которое отслеживается мышью. Оно примерно равно 1/200 дюйма.

Таблица 3.3. Функции драйвера мыши.

Bios INT 33h

Функция 00h - инициализировать драйвер мыши

Вход:     AX:         0000h

Выход: AX:         FFFFh в случае успеха,

0000h при неудаче

ВХ - количество кнопок мыши

Функция 01h

- включить курсор мыши

Вход:  AX: 0001h

Выход: Ничего

Функция 02h - выключить курсор мыши

Вход:   AX: 0002h

Выход: Ничего

Функция 03h - возвратить позицию курсора и статус клавиш

Вход:   АХ:000Зh

Выход: ВХ - статус кнопок

Бит 0 - левая кнопка: 1 - нажата, 0 - не нажата

Бит 1 - правая кнопка: 1 - нажата, 0 - не нажата




Бит 2 - центральная кнопка: 1 - нажата, 0 - не нажата

СХ -  Х-координата курсора

DX - Y-координата курсора

Функция 0Bh - возвратить относительную позицию мыши

Вход:     AX: 000Bh

Выход:  СХ - относительное горизонтальное движение в mickey

DX - относительное вертикальное движение в mickey

Функция 1Ah - установить чувствительность

Вход: AX: 00lAh

Выход: ВХ - чувствительность по оси Х (0-100)

СХ - чувствительность по оси Y (0-100)

DX - значение скорости, при которой чувствительность возрастает в 2 раза (0-100)

Как видите, функции драйвера вызываются через прерывание 33h. Мы записываем параметр в регистр АХ .и получаем результат в регистрах АХ, ВХ, СХ и DX. Я написал простую функцию для работы с мышью, она называется Squeeze_Mouse(). Эта функция может выполнять много действий — все зависит от передаваемых параметров. Прототип функции:

int Squeeze_Mouse(int command, int *x, int *y, int *buttons) ;

Кроме этого, я сделал несколько описаний, чтобы упростить работу с ней:

#define MOUSE_INT             0х33 // номер

прерывания

#define MOUSE_RESET           0х00 // сброс

мыши

#define MOUSE_SHOW            0х01 // показать

мышь

#define MOUSE_HIDE            0х02 // погасить

мышь

#define MOUSE BUTT_POS        0х03 // возвратить координаты

//и количество кнопок

#define MOUSE_SET_SENSITIVITY 0x1A // установить

//чувствительность

//в пределах 0-100

#define MOUSE_MOTION_REL      0x0B // установить

// относительную

// чувствительность

Таким образом, если мы хотим получить координаты мыши, то должны написать следующее:

Squeeze Mouse(MOUSE_BUTT_POS, &mouse_x, &mouse_y, &mouse_buttons);

где mouse_x, mouse_y и mouse_buttons - локальные переменные для сохранения результатов.

Теперь обратим внимание на два способа, используемые для передачи координат мыши:

§          Драйвер мыши может возвращать абсолютные координаты. В этом случае значения Х и Y являются координатами мыши на экране.


К примеру, если мышь находится в левом верхнем углу экрана, функция возвращает (0,0);

§          Драйвер мыши может возвращать относительные координаты. При этом возвращается разница координат от предыдущей посылки. Например, если мышь подвинулась на 20 микки по оси Х и на 10 по оси Y, то эти значения и будут возвращены. Для чтения в относительном режиме используйте константу MOUSE_MOTION_REL.

Еще несколько слов о мыши. Вы можете менять ее чувствительность к передвижению, используя константу MOUSE_SET_SENSITIVITY. Для этого подберите для переменных Х и Y значение от 1 до 100 и вызовите Squeeze_Mouse. Чувствительность мыши определяется как отношение пиксельного перемещения курсора мыши к одному микки.

Листинг 3.8 содержит демонстрационную программу, которая показывает использование мыши. Эта программа позволяет рисовать на экране, нажимая на левую кнопку мыши и менять цвет, используя правую кнопку.

Листинг 3.8. Работа с мышью (MOUSE.C).

// ВКЛЮЧАЕМЫЕ ФАЙЛЫ ///////////////////////////////////////

#include <dos.h>

#include <bios.h>

#include <stdio.h>

#include <math.h>

#include <conio.h>

#include <graph.h>

// ОПРЕДЕЛЕНИЯ ///////////////////////////////////////////

// вызовы функций мыши

#define MOUSE_INT             0х33 // Номер прерывания мыши

#define MOUSE_RESET           0х00 // Инициализация драйвера

#define MOUSE_SHOW            0х01 // Показать курсор мыши

#define MOUSE_HIDE            0х02 // Спрятать курсор мыши

#define MOUSE_BUTT_POS        0х03 // Получение полного статуса

#define MOUSE_SET_SENSITIVITY Ox1A // Установка

чувствительности

#define MOUSE_MOTION_REL      ОхОВ // Получить значение счетчика

     // микки

#define MOUSE_LEFT_BUTTON     0х01 // левая

кнопка

#define MOUSE_RIGHT_BUTTON    0х02 // правая

кнопка

#define MOUSE_CENTER_BUTTON   0х04 // средняя

кнопка

// ФУНКЦИИ ////////////////////////////////////////////////

int Squeeze_Mouse(int command, int *x, int *y,int *buttons)



{

// Мы будем использовать _int86 вместо встроенного ассемблера

// Почему? И сам не знаю

union _REGS inregs, outregs;

switch(command) {

case MOUSE_RESET:

{

inregs.x.ax = 0х00; // подфункция 0 – инициализация

_int86(MOUSE_INT, &inregs, &outregs};

*buttons = outregs.x.bx; // возвратить количество кнопок

return(outregs.x.ax);    // возвратить общий результат

} break;

case MOUSE_SHOW:

     {

// эта функция инкрементирует счетчик драйвера. Когда значение

// счетчика становится больше или равно 0, курсор появляется

// на экране

inregs.х.ах = 0х01; // подфункция 1 - показать курсор

_int86(MOUSE_INT, &inregs, &outregs);

return (1);

} break;

case MOUSE_HIDE:

{

// эта функция декрементирует счетчик драйвера; когда его

// значение становится меньше 0, курсор исчезает с экрана

inregs.х.ах = 0х02; // подфункция 2 - спрятать курсор

_int86(MOUSE_INT, &inregs, &outregs);

return(1);

} break;

case MOUSE_BUTT_POS:

{

// эта функция  позволяет получить полный статус состояния мыши,

// включая абсолютную позицию курсора в координатах (х,у) и

// состояние кнопок

inregs.х.ах = 0х03; // подфункция 3 - получить статус мыши

_int86(MOUSE_INT, &inregs, &outregs);

// извлечь информацию и вернуть ее через указатели

*х       = outregs.х.сх;

*у       = outregs.x.dx;

*buttons = outregs.x.bx;

return(1);

} break;

case MOUSE_MOTION_REL:

{

// эта функция позволяет получить относительное изменение

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

inregs.х.ах = 0х03; // подфункция 1 – получить

  // относительную позицию

_int86(MOUSE_INT, &inregs, &outregs);

// результат при помощи указателей помещается в переменные х и у

*х       = outregs.x.cx;

*у       = outregs.x.dx;

return(1);

} break;

case MOUSE_SET_SENSITIVITY:

{

// эта функция устанавливает чувствительность мыши. Перед

// вызовом необходимо установить переменные х и у в значения

// из диапазона 1-100. Переменная "buttons" используется для

// установки значения порога удвоения скорости



// (из диапазона 1-100)

inregs.x.bx = *х;

inregs.x.cx = *у;

inregs.x.dx = *buttons;

inregs.x.ax = 0x1A; // подфункция 26 - установка

  //  чувствительности

_int86(MOUSE INT, &inregs, &outregs);

return(1);

} break;

default:break;

} // конец оператора switch

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

// ОСНОВНАЯ ПРОГРАММА /////////////////////////////////////////////////////////////////////////////

void main(void)

{

int x,y,buttons,num_buttons;

int color=l;

_setvideomode(_VRES16COLOR};  // 640х480, 16 цветов

// инициализация драйвера мыши

Squeeze_Mouse(MOUSE_RESET,NULL,NULL,&num_buttons);

//показать

курсор

Squeeze_Mouse(MOUSE_SHOW,NULL,NULL,NULL);

while(!kbhit())

{

_settextposition(2,0);

Squeeze_Mouse(MOUSE_BUTT_POS,&x,&у,&buttons);

printf("mouse x=%d y=%d buttons=%d    ",х,у,buttons) ;

// рисование

if (buttons==1)

{

_setcolor(color) ;

_setpixel(x-1,y-2);

_setpixel(х,y-2) ;

_setpixel(x-1,y-1);

_setpixel(х,y-1);

} // конец обработки нажатия левой кнопки

// выбор

цвета

if (buttons==2)

{

if (++color>15) color=0;

// ждем отпускания правой кнопки

while(buttons==2)

{

Squeeze_Mouse(MOUSE_BUTT_POS,&x,&y,&buttons) ;

} // конец ожидания

} // конец графической работы

} // конец цикла while

// назад в текстовый режим

_setvideomode(_DEFAULTMODE);

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


Начальная и конечная заставки


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

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

В начальной заставке вы можете:

§

Дать более детализированное изображение главного героя игры;

§          Показать основного врага, продемонстрировав весь его устрашающий потенциал и как бы говорящего: «Приди и попробуй меня победить»;

§          Нарисовать несколько монстров из вашей игры в атакующих позах;

§          Еще один неплохой выбор — использовать несколько видеокадров, выполненных в том же разрешении, что и сама игра.

Для конечной заставки может применяться та же техника. Главное назначение этой заставки — пробудить у игрока желание вернуться в игру.



Написание программы обработки прерываний на языке Си


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

Допустим, мы хотим создать обработчик прерывания, который планируем связать с прерыванием таймера - его номер 0х1С. Начать можно вот так:

void _interrupt _far Timer (void)

{

} // окончание функции timer

Ключевое слово _far необходимо потому, что все прерывания представляют собой 32-разрядные вызовы (смещение и сегмент).

В пределах функции вы можете делать все, что вам заблагорассудится с единственным условием: весь ваш код должен быть рентабельным. Это значит, что вы должны быть крайне осторожны при обращении к функциям DOS или сложным функциям Си типа printf. Я не рекомендую вам использовать в подобных функциях расширения языка Си. О сохранении содержимого регистров можно не беспокоиться: Си-компилятор запоминает содержимое регистров перед началом обработки прерывания и восстанавливает по окончании. Это касается всех регистров за исключением регистра сегмента стека SS. И еще, с целью облегчения доступа к данным при входе в обработчик прерывания регистр DS указывает на глобальный сегмент данных вашей программы, благодаря чему программа обработки прерываний имеет доступ к глобальным переменным. Звучит слишком заманчиво, чтобы быть правдой, однако на самом деле все организовано именно так.

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



Несколько слов по поводу цвета


С точки зрения художников, большинство оттенков может быть получено из тpex первичных цветов: красного, желтого и синего. Если вы смешиваете эти три краски в одинаковых пропорциях, то вы получите цвет, очень близкий к черному. Единственный цвет, который нельзя получить смешением красок - это белый.                                    

В компьютерном мире, где цвет «соткан» из света, различные оттенки получаются путем изменения уровней красного, зеленого и синего цвета. Многие программы описывают цвета на вашем экране, представляя их тремя номерами, отделяемыми запятой. Например, черный цвет может быть представлен как R0, G0, В0 (это означает, что для данного цвета все три составляющие цвета отключены). Белый цвет создается путем установки всех трех значений на максимум (R63, G63, В63 - для 8-битных и R255, G255, В255 - для 24-битных регистров).

Цвета могут быть условно разделены на «холодные» и «теплые». В таблице 16.1 показано несколько примеров.

Таблица 16.1 «Температуры» цветов.

Теплые цвета           Холодные цвета

красный                         зеленый

желтый                           синий

оранжевый                   фиолетовый

земляные краски        все оттенки серого, включая белый и черный

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

Стоит упомянуть еще вот о чем. Любое изображение может восприниматься соверщенно по-разному в зависимости от цвета фона. Чтобы увидеть это на примере, просмотрите файл ЕХАМР16.РСХ (который вы можете найти на дискете). Этот файл показывает двух чудовищ, одного серого, а другого красного, которые помещены на светлосером и белом фоне. В обоих случаях чудовища имеют один и тот же цвет, но, будучи помещенными на белом фоне, выглядят темнее.

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



Несколько смещающихся слоев


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

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

Не стоит пытаться вычислять точные скорости смещения (если вы сделаете это, то, скорее всего, получите число с плавающей запятой, а по известным причинам скорость смещения должна иметь целочисленное значение). В действительности демонстрационная программа параллакса из Листинга 17.3 использует простое правило для скоростей смещения: каждый следующий слой смещается вдвое медленнее, чем слой впереди него. Запомните: важно только то, чтобы относительное движение между различными слоями обеспечивало ощущение глубины пространства.

Изображение строится слой за слоем от заднего к переднему плану. В результате ближние слои перекрывают и прячут некоторые части более удаленных (этот метод, известный также как Алгоритм Художника, детально обсуждался в шестой главе, «Третье измерение»). Такое сокрытие дальних слоев ближними создает ощущение перспективы. Что же касается площади перекры­тия, то она зависит от изображений. Функция OpaqueBIt() может быть использована также и для рисования перекрывающихся слоев. Плохо только, что при выводе изображения она стирает имеющуюся картинку. Это не очень практично для большинства типов декорации. Поэтому нам нужно научиться рисовать образы так, чтобы они закрывали собой уже выведенное изображение не полностью, а лишь по контуру и чтобы вся внешняя область оставалась без изменения.



Net Tank: танковый имитатор для двух игроков


Net-Tank — игра для одного или двух игроков. Я написал ее, чтобы показать как создаются игры для нескольких участников и осветить некоторые проблемы которые могут при этом возникнуть. Это самая примитивная игра (для её написания мне понадобилось всего три дня), имеющая один-единственный уровень. Более того, это двухмерная игра, в которой игрок смотрит на поле боя сверху вниз. Мы напишем полноценную трехмерную игру типа Wolfenstetn в девятнадцатой главе, а сейчас я хочу оставить программу достаточно простой чтобы акцентировать внимание не на алгоритмах игровой логики, а на коммуникационной части.

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

SNDLIB.C          библиотека звуковых эффектов;

GRAPH1.С         низкоуровневый двухмерный спрайт и графическая библиотека;

KEYLIB.C          библиотека ввода с клавиатуры;

SERLIB.C          коммуникационная библиотека.

Все файлы заголовков для этих библиотек имеют те же самые имена:

SNDLIB.H GRAPH1.H KEYLIB.H SERLIB.H

Все эти модули объединяются в одну обширную библиотеку с помощью менеджера LIB.EXE. Для создания этой библиотеки вам надо:

1. Скомпилировать каждый Си-модуль с помощью файла СО.ВАТ:

cl -AM  -Zi  -с  -Fc -Gs -G2 %1.с

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

lib mylib.lib

3. Программа скажет, что библиотека с таким именем отсутствует, и спросит вашего согласия на ее создание. Ответьте утвердительно;

4. Дрбавьте в командную строку все модули, входящие в библиотеку, как это укаэано:                                            




operations: +SNDLIB +GRAPH1 +KEYLIB +SERLIB              

5. Затем появятся еще два запроса. Ответьте на них нажатием клавиши Enter.

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

Для создания игры, вам необходимо создать два исполняемых модуля:

§

Один для игрока 1;

§          Другой для игрока 2.

Чтобы это сделать, вам надо:

1. Откомпилировать NET1.C и NET2.C (2 версии игры) следующим с помощью командного файла СС.ВАТ:

cl -AM -Zi -с -Fc -Gs -G2 %1.С

if errorlevel 1 goto с fail

link  /ST:16384 /CO %1/,,,graphics.lib+myiib.iib,,

:c_fail

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

NET1.EXE и NET2.EXE;

2. Поместите NET1.EXE ,на ПК1 и NET2.EXE на ПК2. Соедините обе машины нуль-модемным кабелем через СОМ1;

3. Теперь можно начинать игру. Наберите NET1.EXE на ПК1 и NET2.EXE на ПК2. Машины соединятся и вы можете сразиться со своим приятелем на танковой дуэли.

Используйте следующие управляющие клавиши:

Стрелка вправо     Повернуть направо

Стрелка влево       Повернуть налев

Стрелка вверх      Двигаться вперед

Стрелка вниз        Двигаться назад

Esc                         Выход из игры

Пробел                   Стрельба

Т                              Подразнить партнера

Поиграйте в Net-Tank и попутно обратите внимание на следующие вещи:

§          Возникает ли эффект запаздывания при перемещениях?

§          Теряет ли игра синхронизацию?                     

§          Если да, то в какие моменты? 

Также обратите внимание на звуковые эффекты. Для их создания я использовал собственный голос и условно-бесплатную программу Blaster Master Наконец, игру можно проводить при установленной программе-ускорителе клавиатуры.Если при нажатии клавиши танк слишком резво устремляется вперед, попробуйте уменьшить скорость реагирования клавиатуры, установив программу TURBOKEY.COM, которая имеется на дискете.


Новая версия масштабирования


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

Механизм масштабирования мы показали в седьмой главе, «Улучшенная битовая графика и спецэффекты» (Листинг 7.9). Для масштабирования текстуры стен в этом алгоритме был использован не совсем обычный подход. Каждая стена рассматривалась не как обычная двухмерная матрица, имеющая высоту и ширину, а разбивалась на отдельные одномерные столбцы. В результате программе нужно было масштабировать только столбцы шириной в один пиксель. (Кстати, простой текстурированный пиксель называется текстелем (textel).)

Для реализации двухмерного масштабирования мы слегка модифицируем код, представленный в Листинге 7.9. В новом варианте будут использованы только целые числа, а индексы масштабирования будут предварительно подсчитаны и занесены в таблицу соответствий. Текст новой программы масштабирования приведен в Листинге 8.1.

Листинг 8.1. Новая функция масштабирования спрайтов (без отсечения).

void Scale_Sprite(sprite_ptr sprite,int scale)

//Эта функция масштабирует спрайт (без отсечения). Масштабирование

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

// которая определяет, как будет изменяться каждый вертикальный

// столбец. Затем другая таблица используется для учета

// масштабирования этих столбцов по оси Х

char far *work_sprite; // текстура

спрайта

int *row_у;             // указатель на масштабированные

         // по оси Y

данные (заметьте, что это

// ближний указатель)

int far

*row_x;         // указатель на масштабированные

   // по оси Х данные (заметьте, что это

// дальний указатель)

unsigned char pixel;    // текущий текстель

int    x,                   // рабочие переменные

y,

column, work_offset, video_offset, video_start;

// если объект слишком мал, то и рисовать его не стоит




if (scale<1) return;

// рассчитываем необходимые для масштабирования данные

row_у = scale_table_y [scale];

row_x = scale_table_x[scale];

// выбираем соответствующий кадр спрайта

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

// рассчитываем

начальное смещение

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

// изображение рисуется слева направо и сверху вниз

for (х=0; x<scale; x++)

{

// пересчитываем адрес следующего столбца

video_offset = video_start + x;

// определяем, какой столбец должен быть отображен,

// исходя из индекса масштабирования по оси Х

column = row_x[x];

// наконец рисуем столбец обычным образом

for (y=0; y_scale; y++)

{

// проверка на "прозрачность"

pixel = work_sprite[work_offset+column];

if (pixel)

double_buffer[video_offset] = pixel;

// индекс следующей строки экрана и смещение в области

// хранения

текстуры

video_offset += SCREEN_WIDTH;

work_offset = row_y[y] ;

} // конец цикла по У

} // конец цикла по Х

} // конец Scale_Sprite

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

Отметим, что таблицы соответствия находятся в разных сегментах памяти: ближнем (NEAR) и дальнем (FAR). Это сделано для скорости. Таблица соответствия во внутреннем цикле масштабирования (по оси У) должна быть в ближнем сегменте данных для ускорения доступа. Таблица соответствия во внешнем цикле (масштабирование по оси X) может располагаться в дальнем сегменте, так как доступ к ней обычно осуществляется только несколько десятков раз.

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


Основное правило при работе с таблицами соответствий на языке Си состоит в следующем: пытайтесь расположить наиболее часто используемые таблицы в ближнем сегменте, а менее употребимые — в дальнем. Это правило не касается того случая, когда есть возможность уместить и таблицу, и глобальные переменные в 64К ближнего сегмента данных.

Теперь посмотрим на нашу новую процедуру масштабирования в действии. Я написал программу, которая загружает ряд заранее отсканированных изображений размером 80х48 пикселей. Эти изображения были сделаны с помощью макета космического корабля, который фотографировался под разными углами в моей импровизированной студии (о ней я уже рассказывал в начале главы). Эта программа (VYREN.C) показывает вращающийся на некотором расстоянии от наблюдателя (то есть от вас) космический корабль и позволяет передвигать его по оси Z с помощью клавиш > (правая угловая скобка или «меньше») и < (левая угловая скобка или «больше»), (На самом деле, в нижнем регистре это будут клавиши с символами запятой и точки соответственно.) Вы увидите, что новый метод с использованием таблиц существенно быстрее прежнего.

Текст этой программы приведен в Листинге 8.2. Потратьте некоторое время и постарайтесь понять, как работает эта программа. Как обычно, программа должна быть скомпонована с нашей растущей графической библиотекой, GRAPHICS.С, и скомпилирована с использованием модели памяти MEDIUM. Для завершения работы программы, следует нажать клавишу Q.

Листинг 8.2. Демонстрационная программа новой функции масштабирования (VYREN.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 <graph.h>

#include "graphics.h" // включаем нашу графическую библиотеку



// ПРОТОТИПЫ ////////////////////////////////////////////////////////

void Create_Scale_Data_X(int scale, int far *row);

void Create_Scale_Data_Y(int scale, int * row);

void Build_Scale_Table(void);

void Scale_Sprite(sprite_ptr sprite,int scale);

void Clear_Double_Buffer(void) ;

// ОПРЕДЕЛЕНИЯ /////////////////////////////////////////////

#define MAX_SCALE      200      // число звезд на звездном небе

#define SPRITE_X_SIZE  80       // максимальные размеры

#define SPRITE_Y_SIZE  48       // растрового изображения

// ГЛОБАЛЬНЫЕ ПЕРЕМЕННЫЕ ///////////////////////////////////

sprite object;                       // обобщенный спрайт, который

// содержит кадры с космическим

// кораблем

pcx_picture text_cells;// PCX-файл с изображениями

int *scale_table_y[MAX_SCALE+l];     // таблица с предварительно

// рассчитанными коэффициентами

// масштабирования

int far *scale_table_x[MAX_SCALE+l]; // таблица с предварительно

          // рассчитанными коэффициентами

          // масштабирования

// ФУНКЦИИ /////////////////////////////////////////////////

void Create_Scale_Data_X(int scale, int far *row)

{

// эта функция масштабирует полосу текстуры для всех возможных

// размеров и создает огромную таблицу соответствий

int х;

float x_scale_index=0, x_scale_step;

// рассчитываем шаг масштабирования или число исходных пикселей

// для отображения на результирующее изображение за цикл

x_scale_step = (float)(sprite_width)/(float)scale;

x_scale_index+=x_scale_step;

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

{

// помещаем данные в массив для последующего использования

row[x] = (int)(x_scale index+,5);

if  (row[x] > (SPRITE_X_SIZE-1)) row[x] = (SPRITE_X_SIZE-1);

// рассчитываем следующий индекс

x_scale index+=x_scale_step;

} // конец

цикла

} // конец Create_Scale_Data_X

///////////////////////////////////////////////////////////

void Create_Scale_Data Y(int scale, int *row)

{

// эта функция масштабирует полосу текстуры для всех возможных

// размеров и создает огромную таблицу соответствий



int у;

float y_scale_index=0, у

scale_step;

// рассчитываем шаг масштабирования или число исходных пикселей

// для отображения на результирующее изображение за цикл

у_scale_step = (float)(sprite_height)/(float)scale;

y_scale index+=y_scale_step;

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

{

// помещаем данные в-массив для последующего использования

row[y] = ((int)(y_scale_index+.5)) * SPRITE_X_SIZE;

if (row[y] > (SPRITE_Y_SIZE-1)*SPRITE_X_SIZE) row[y] = (SPRITE_Y_SIZE-1)*SPRITE_X_SIZE;

// рассчитываем следующий индекс

y_scale_index+==y_scale_step;

} // конец

цикла

} // конец Create_Scale_Data_Y //////////////////////////////////////////

void Build_Scale_Table (void)

{ // эта функция строит таблицу масштабирования путем расчета

// коэффициентов  масштабирования для всех возможных размеров

// от 1 до 200 пикселей

int scale;

// резервируем память

for (scale=l; scale<=MAX_SCALE; scale++)

{

scale_table_y[scale] = (int *)malloc(scale*sizeof(int)+1);

scale_table_x[scale] = (int far *)_fmalloc(scale*sizeof(int)+l);

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

// создаем таблицу масштабирования для осей X и Y

for (scale=l; scale<=MAX_SCALE; scale++) {

// рассчитываем коэффициент для данного масштаба

Create_Scale_Data_Y(scale, (int *)scale_table_y[scale]);

Create_Scale_Data_X(scale, (int far *)scale_table_x[scale]) ;

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

}// конец Build_Scale_Table ////////////////////////////////////////////////////////////

void Scale_Sprite(sprite_ptr sprite,int scale)

{

// эта функция масштабирует спрайт (без отсечения). Масштабирование производится с //использованием заранее рассчитанной таблицы, которая определяет, как будет изменяться //каждый вертикальный столбец. Затем другая таблица используется для учета //масштабирования этих столбцов по оси Х

char far *work_sprite;  // текстура спрайта

int *row_y;             // указатель на масштабированные

// по оси Y данные (заметьте, что

// это ближний указатель)

int far *row_x;         // указатель на масштабированные

// по оси Х данные (заметьте, что



// это дальний указатель)

unsigned char pixel;    // текущий текстель

lnt x,                  // рабочие переменные

У, column,

work_offset,

video_offset,

video_start;

// если объект слишком мал, то и рисовать его не стоит

if (scale<1) return;

// рассчитываем необходимые для масштабирования данные

row_y = scale_table_y[scale];

row_x = scale table_x[scale];

// выбираем соответствующий кадр спрайта

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

// рассчитываем начальное смещение

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

// изображение рисуется слева направо и сверху вниз

for (x=0; x<scale; х++)

{

// пересчитываем адрес следующего столбца

video_offset = video_start + х;

// определяем, какой столбец должен быть отображен,

// исходя из индекса масштабирования по оси Х

column = row_x[x];

// Наконец рисуем столбец обычным образом

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

{

// проверка на "прозрачность"

pixel = work_sprite[work_offset+column] ;

if (pixel)

double buffer[video_offset] = pixel;

// индекс следующей строки экрана и смещение

//в области хранения текстуры

video_offset += screen_width;

work_offset  =  row_y[y];

} // конец цикла по Y

} // конец цикла ро Х

} // конец Scale_Sprite

//////////////////////////////////////////

void Clear Double_Buffer(void) {

// угадали что это?

_fmemset(double_buffer, 0, SCREEN__WIDTH * SCREEN_HEIGHT + 1);

} // конец Clear_Double_Buffer

// ОСНОВНАЯ ПРОГРАММА //////////////////////////////////////

void main(void)

{

// Загружаем 12 предварительно отсканированных кадров спрайта

// и последовательно меняем их до тех пор пока игрок не изменит

// координату Z

объекта, нажав клавишу "," или "."

int done=0,                    // флаг завершения

count=0,                   // счетчик времени изменения кадра

scale=64;                  // текущий масштаб спрайта

float scale_distance

= 24000, // произвольная константа

// для согласования



// плоской текстуры и трассированного

// пространства

view_distance

= 256, // дистанция до объекта

х=0,                      // позиция корабля в трехмерном

            // пространстве

у=0,

z=1024;

// установка видеорежима 320х200х256

_setvideomode(_MRES256COLOR) ;

sprite_width = 80;

sprite_height =48;

// создание таблицы для подсистемы масштабирования

Build_Scale_Table ();

// инициализация файла PCX, содержащего кадры

PCX_Init((pcx_picture_ptr)&text cells) ;

// загрузка файла PCX, содержащего кадры

PCX_Load("vyrentxt.pcx", (pcx_picture_ptr)&text_cells,1);

// резервируем память под дублирующий буфер

Init_Double_Buffer ();

Sprite_Init((sprite_ptr)&object,0,0,0,0,0,0) ;

// загружаем 12 кадров с космическим кораблем

PCX_Grap_Bitmap ( (pcx_picture_ptr) &text_cells,

(sprite_ptr)&object,0,0,0);

PCX_Grap_Bitmap((pcx_picture_ptr)&text_cells,

(sprite_ptr)&object,1,1,0) ;

PCX_Grap_Bitmap((pcx_picture_ptr)&text_cells,

(sprite_ptr)&object,2,2,0) ;

PCX_Grap_Bitmap ((pcx_picture_ptr)&text_cells,

(sprite_ptr)&object,3,0,1);

PCX_Grap_Bitmap((pcx_picture_ptr)&text_cells,(sprite_ptr)&object,4,1,1) ;

PCX_Grap_Bitmap ((pcx_picture_ptr)&text_cells,

(sprite_ptr)&object,5,2,1);

PCX_Grap_Bitmap((pcx_picture_ptr)&text_cells,

(sprite_ptr)&object,6,0,2);

PCX_Grap_Bitmap((pcx_picture_ptr)&text_cells,

(sprite_ptr)&object,7,1,2) ;

PCX_Grap_Bitmap((pcx_picture_ptr)&text_cells,

(sprite_ptr)&object,8,2,2) ;

PCX_Grap_Bitinap( (pcx_picture_ptr)&text_cells,

(sprite_ptr)&object,9,0,3);

PCX_Grap_Bitmap((pcx_picture_ptr)&text_cells,

(sprite_ptr)&object,10,1,3);

PCX__Grap_Bitmap( (pcx_picture_ptr) &text_cells,

(sprite_ptr)&object,11,2,3) ;

// начальная позиция корабля

object.curr_frame =0;

object.x          = 0;

object, у          =0;

Clear_Double_Buffer();

// ожидаем нажатия клавиш и рисуем корабль

while(!done)

{

// нажал ли игрок клавишу?



if (kbhit())

{

switch(getch()) {

case '.': // отдаляем корабль

{

z+=16;

} break;  

case ',':// приближаем корабль

{

z-=l6;

// не позволяем кораблю подойти слишком близко

if(Z<256)

z=256;

} break;

case

'q': // выход из программы

{

done=1;

} break;

default:break;

} // конец оператора switch

} // конец оператора if

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

scale = (int)( scale_distance/z );

// исходя из размера растрового изображения,

// рассчитываем проекции координат Х и Y

object.x= (int)((float)x*view_distance / (float)z) + 160 - (scale>>1);

object.y = 100  - (((int)((float y*view_distanc=e / (float)z) + (scale>>1)));

// увеличиваем счетчик кадров

if

(++count==2)

{

count=0;

if (++object.curr_frame==12) object.curr_frame=0 ;

} // конец оператора if

// очищаем дублирующий буфер

Clear_Double_Buffer();

// масштабируем

спрайт

Scale_Sprite((sprite_ptr)&object,scale);

Show_Double_Buffer(double_buffer);

// выводим информацию на экран

_settextposition(24,0) ;

printf("z Coordinate is %f",z);

} // конец оператора while

// Удаляем файл PCX

PCX_Delete((pcx_picture_ptr) &text_cells);

// восстанавливаем текстовый режим

_setvideomode(_DEFAULTMODE);

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

После выполнения программы из Листинга 8.2, вы, возможно, будете удивлены возможностями оцифровки изображений макетов и приведенным вариантом трехмерной мультипликации. Кто знает, может быть вы создадите что-нибудь именно по типу игры Wing Commander, а вовсе не очередную вариацию DOOM? Однако пора переходить к алгоритму отсечения.


Ну дайте мне место (разработка стен, пола и «кирпичиков» потолка!


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

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



Об этой книге


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



Объекты


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

Листинг 4.4. Структуры данных для задания вершин объектов.

typedef struct vertex_typ

{

float x,y;

} vertex, *vertex_ptr;

// структура

объекта

typedef struct object_typ

{

int num_vertices;     // количество вершин в объекте

int color;            // цвет объекта

float хо,уо;          // позиция объекта

float x_velocity;     // используем позже для

float y_velocity;     // перемещения объекта

float scale;          // коэффициент масштабирования

float angle;          // угол поворота

vertex vertices[16];  // массив для определения 16 вершин

} object, *object_ptr;

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



Объем просмотра


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

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

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

Я больше ничего не буду говорить про трехмерное отсечение - оно не потребуется для игр типа DOOM и Wolfenstem, но может пригодиться для создания различных «леталок» и «ездилок». Вы можете использовать ту же тактику, что и для двухмерного отсечения. Теперь нам надо познакомиться еще с одной вещью — моделированием цельных геометрических объектов.



Общая матрица масштабирования, поворотов и перемещений


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

Если вы теперь умножите вершины объекта на эту матрицу, то получите перемещенный, повернутый и масштабированный объект. Не слабо, а?



Общий вид ассемблерной функции


Процедуры ассемблера очень похожи на Си-функции. У них есть начало, конец и код, расположенный в середине. В этой книге мы будем ссылаться на Microsoft macro assembler версий от 5.1 до 6.1 (MASM). Это связано с тем, что.в них уже есть директивы, упрощающие стыковку с программами на Си.

Примечание

MASM версии 5.0 подойдет для первого примера, но для успешной работы с книгой вам понадобится версия 5.1 или старше.

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

Листинг 2.1. Прототип процедуры для MASM 5.0 и более старших версий.

.MODEL MEDIUM                ;тип модели памяти

.CODE

; начало кода

PUBLIC _function_name      ;информация для компоновщика. Функция             

;может экспортироваться

_function_name PROC FAR      ;название и тип функции(ближняя

;или дальняя). Ближние функции можно использовать для моделей

;памяти SMALL и COMPACT, а дальние применяются для моделей

;памяти MEDIUM, LARGE и HUGE.

push ВР                      ;готовим фрейм стека–пролог

;функции

mov BP,SP                    ;сохраним стек

;Работа функции

pop ВР                       ;восстанавливаем фрейм стека

;эпилог функции

_function_name ENDP          ; конец процедуры

END

;конец кода

Давайте проанализируем программу, приведенную в Листинге 2.1.

§          Первая директива, которую мы встречаем — это .MODEL. Как и компилятор Си, MASM должен знать, какая из моделей памяти используется. Ключевое слово MEDIUM означает, что мы собираемся использовать модель памяти именно типа MEDIUM. Теперь я хочу напомнить вам свойства основных моделей памяти:

·         Модель SMALL имеет один 64-килобайтный сегмент для кода и один сегмент для данных;

·         Модель COMPACT имеет один 64-килобайтный сегмент для кода и несколько сегментов данных;




·         Модель MEDIUM имеет один 64-килобайтный сегмент для данных и несколько сегментов для кода;

·         Модель LARGE имеет несколько сегментов как для кода, так и для данных;

·         Модель HUGE разрешает данным быть больше, чем 64К,но в остальном полностью похожа на модель LARGE.



Чаще всего мы будем использовать модели памяти MEDIUM и LARGE.

§          Следующая директива — PUBLIC. Она говорит MASM, что следующее имя будет экспортировано, то есть станет «видимо» из других модулей;

§          Теперь мы переходим к началу самой функции. В ассемблере функция начинается с директивы PROC, которая следует сразу за именем функции;

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

§          В конце процедуры мы очищаем стек;

§          В конце каждой процедуры ставится ключевое слово ENDP;

§          В одном блоке мы можем иметь сколько угодно процедур, но надо помнить, что самой последней должна быть директива END. Она сообщает ассемблеру об окончании программы.

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


Семейство процессоров 80х86 более чем


Семейство процессоров 80х86 более чем разнообразно. Фирма Intel создала процессоров больше, чем вы в состоянии представить. Игры и программы, которые мы будем создавать в этой книге, ориентированы только на 386-, 486- и 586-процессоры. Для наших целей мы будем говорить о реальном режиме работы этих процессоров: режим эмуляции процессора 8086, используемый DOS с 640-килобайтным ограничением.

Как вы знаете, первым из этого семейства был 8088. Он стоял на первом ПК. Потом его сменил 8086, но это уже история. Весь мир перешел на DOS которая изначально была ориентирована на процессор 8086. Производя процес­соры 286, 386, 486, 586 (и вроде скоро появится 686), Intel обеспечивал поддержку 8086-процессора. Это значит, что даже на 586 ПК вы неизбежно натолкнетесь в DOS'e на 640-килобайтный барьер.

На самом деле, это ужасно, поскольку 386 и все последующие 32-разрядные процессоры имеют непрерывную модель памяти (без сегментов), более мощные инструкции и т. д. Использование только новых инструкций сделает наши игры более мощными. Более того, непрерывная модель памяти устранит понятие сегментных регистров, 640-килобайтного барьера и всего, что с ним связано. Мы можем указать компилятору порождать 286-инструкции, дав соответствующую директиву, но это будут лишь 16-битные команды, которые не дадут нам возможности использования всех средств 32-разрядных процессоров типа 386 486 и 586.

Кстати, для удобства я буду называть 386-, 486- и 586-процессоры просто «процессорами».

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


Обзор VGA-карт


VGA-карта это плата, которая генерирует видеосигнал, передаваемый на дисплей. Этот видеосигнал состоит из серии импульсов, определяющих цвет и интенсивность каждой точки на экране. Не будем вдаваться в подробности о том, как он образуется или как карта VGA создает все временные задержки для корректной работы с дисплеем. У нас есть единственный вопрос: как мы можем поместить пиксель нужного цвета на экран?

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

Как вы знаете, VGA-карта поддерживает много различных графических режимов и может быть перепрограммирована для получения совершенно фантастических разрешений (например, я создал режим 320х400, 256 цветов путем прямого перепрограммирования временных регистров VGA-карты). Такая функциональность стала возможной благодаря архитектуре VGA-карты. Карта VGA имеет ряд регистров, которые можно по-разному программировать для решения различных задач. В любом случае, программирование регистров VGA — это тема для отдельной книги. Если вы этим огорчены, не отчаивайтесь, существует множество книг на эту тему. В моей маленькой домашней библиотеке более 30 книг посвящены VGA-картам. Если вы хотите узнать больше про секреты VGA, то, я уверен, у вас не возникнет проблем с источниками информации.

Один из путей приблизиться к секретам VGA-карты, это рассматривать ее как систему, поддерживающую только один режим; например, режим, который мы и будем применять при создании наших игр, дающий разрешение 320х200 точек, 256 цветов, называемый режимом 13h. Детали мы обсудим позже в этой главе.

Используя только один из всевозможных графических режимов, мы становимся более независимыми от различий карт VGA. Почему? Наши игры будут работать только на VGA и SVGA и нам не потребуется поддерживать EGA, монохромные и другие карты. Часто разработчики видеоигр пишут десятки драйверов для поддержки разных карт и мониторов- Если мы сосредоточимся на одном режиме, то сможем освободиться от излишней общности и будем писать код только один раз.

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

Мы будем использовать VGA в режиме 13h. Но как этот режим работает? Прочитав следующий раздел, вы найдете ответ на этот вопрос.



Оцифрованный звук


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

Посмотрите на рисунок 9.6. Это графическое изображение того, как я произношу: «Что случилось?» Как вы можете заметить, этот график сильно отличается от графиков чистых тонов, которые мы рассматривали ранее. Спрашивается, откуда же такая разница? Ответ прост: «Это реальный человеческий голос».

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

§

Преобразовать информацию в электронный сигнал;

§          С постоянной частотой дискретизировать этот сигнал.

Во-первых, мы должны конвертировать звук в форму, которую сможем обрабатывать (то есть в цифровую). Это делается с помощью аналогово-цифрового преобразователя- Он преобразует аналоговый сигнал в цепочку цифровых импульсов, состоящих из восьми или шестнадцати битов. Фактически, это означает, что сигнал превращается в последовательность 8- или 16-битных чисел. При использовании восьми битов сигнал может быть представлен числами в - диапазоне от -127 до +128. А если мы используем 16 битов, то диапазон расширяется от -32767 до +32768, что, естественно, дает более высокое качество.

Затем мы должны дискретизировать сигнал с постоянной частотой. Например, представим, что мы дискретизируем разговор человека с частотой 8КГц, используя восемь битов, на протяжении 10 секунд. Это займет 80 кило6айт!!! Как вы;можете видеть, оцифрованный звук требует очень много памяти, вот почему такое большое значение имеют частотные синтезаторы.




Вы можете возразить: « Ну так давайте понизим частоту оцифровки». Это, конечно, можно сделать, но тогда в результате мы потеряем часть данных о звуке вследствие так называемого шума квантования. Пусть наш сигнал имеет частоту 10КГц и мы дискретизируем его с частотой 6КГц. В результате мы не сможем точно воспроизвести форму волны. Практически, а точнее, не столько практически, сколько в полном соответствии со строгим законом Шеннона, вы должны дискретизировать сигнал с частотой в два раза большей, чем его максимальная частота. Только тогда вы сможете затем точно воспроизвести оцифрованный сигнал. Это значит, что если вы хотите точно воспроизвести звук или человеческую речь с частотным диапазоном приблизительно до 20КГц, вы должны дискретизировать его с частотой в два раза большей, то есть 40КГц. Проигрыватели компакт-дисков как раз и работают с частотой 44,1 КГц. На такой скорости вы не потеряете ничего! Звуковые эффекты DOOM, на мой взгляд, оцифрованы где-то на частоте 11 КГц, что являет собой «золотую середину».



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

На рисунке 9.7 изображен процесс дискретизирования сигнала.


Оцифровка объектов и моделирование


Это самая приятная часть. Я чувствовал себя Джорджем Лукасом, создавая макеты и их изображения для этой главы. Хотя это и крайне увлекательное занятие, оно не столь уж и простое, как многие могли бы подумать. (Это касается тебя, Марк!)

Существует три основных подхода к построению моделей и образов для игр:

§

Рисовать их;

§          Моделировать их с помощью трехмерных программ типа 3D Studio;

§          Оцифровывать кадры, полученные с помощью видеокамеры.

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

Теоретически дискретизация миниатюрного макета - довольно простое занятие. Купил видеокамеру или одолжил ее у кого-нибудь, зарядил - и вперед, начинай снимать объект. Это заблуждение! Существует миллион деталей, о которых вы не должны забывать. Освещение, фокус, программное обеспечение, цветовая палитра, разрешение и т. д. - все должно быть сделано, как надо. В своей работе я использовал Creative Labs Video Blaster SE (Special Editions), вместе с Microsoft Video for Windows, Adobe Photostyler и Deluxe Animation фирмы Electronic Art. Кроме этого, я использовал очень дорогую видеокамеру, которую одолжил у своей хорошей знакомой, пообещав упомянуть ее в этой книге. (Диона, я знаю, что на Олимпиаде 1996 года золотая медаль будет твоей!) Наконец, собрав все необходимое, я построил маленькую студию с голубым экраном, вращающейся платформой, освещением и приспособлением для определения угла поворота модели.

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

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



Оформление оценочного экрана


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

§          Зона для показа очков и уровня игры;

§          Зона, которая показывает, какое оружие игрок в данный момент использует;

§          Зона, отражающая состояние здоровья игрока;

§          Зоны, демонстрирующие, сколько боеприпасов, пищи или медикаментов игрок подобрал по дороге;

§          Карта местности, на которой показано местоположение игрока и некоторых важных предметов.



Операции с ROM BIOS


Прежде чем мы начнем писать собственную коммуникационную программу, давайте посмотрим, чем нам может помочь ROM BIOS? Она поддерживает коммуникации через последовательный порт, хотя и весьма ограниченно. Эти функции доступны через прерывание 14h. Существуют функции для открытия последовательного порта, его конфигурирования, чтения и записи символа. Однако существует одна маленькая проблема: эти функции не работают (трудно в это поверить, но они действительно не работают). На самом деле они работают, правда, при особых условиях, которые нам вряд ли удастся создать. К сожалению, отсутствует крайне необходимая нам поддержка ввода/вывода, управляемого прерыванием. В видеоиграх мы должны иметь систему управления событиями, основанную на прерываниях. Из-за этого, а также из-за того, что функции ROM BIOS работают очень медленно, мы можем вообще не рассматривать их как реальное средство передачи битов по проводам.

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



Операция


Результирующие данные

(данные экрана)

Результат

00111000

OR

00001010

00111010=58

00111000

AND

00001010

00001000=8

00111000

XOR

00001010

00110010 =50

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

Однако нам никто не запрещает совершить более одной логической операции с исходными и результирующими данными для достижения желаемого результата. Мы можем делать все, что захотим, но главное при этом — не потерять тот выигрыш в быстродействии, который мы получаем при использовании логических операций. Ведь оператор IF осуществляет только два действия: сравнение и переход в нужное место. Так не окажется ли применение оператора IF более целесообразным, нежели многократное повторение логических операций? Чтобы выяснить это, давайте сравним коды, реализующие оба способа (Листинги 7.1 и 7.2), и посмотрим, как обстоят дела на самом деле.

Листинг 7.1. Дизассемблирование оператора IF.

; File if.с   ; # include <stdio.h>

;

; char far *source, *dest;   //области

исходных

//и результирующих данных

; main()

;{

; Line 8

_main:

*** 000000  c8 00 00 00       enter OFFSET L00181,OFFSET 0

*** 000004  56                push si

*** 000005  57                push di ; index = fffc ; data = fffa

;

;int index;

; Line 10

; unsigned data;

; Line 11

; // оператор if

; if (data=source[index]) ; Line 15       

*** 000006  8b 46 fc          mov  ax, WORD PTR -4[bp]

*** 000009  8b 1e 00 00       mov  bx,WORD PTR  source

*** OOOOOd  8b 0e 02 00       mov  ex,WORD PTR  source+2

*** 000011  03 d8             add  bx,ax

*** 000013  8e cl             mov  es,cx

*** 000015  26 8a 07          mov  al,BYTE PTR es:[bx]

*** 000018  98                cbw

*** 000019  89 46 fa          mov  WORD PTR -6[bp] ,ax

*** 0000lC  3d 00 00          cmp  ax,OFFSET 0



о каждой из этих функций



0

Получить версию драйвера

1

Установить базовый адрес ввода/вывода

2

Установить прерывание DMA

3

Инициализировать драйвер

4

Включить/выключить динамик

5

Установить адрес слова состояния

6

Начать вывод звука

8

Остановить вывод звука

9

Закончить работу драйвера

10

Приостановить вывод звука

11

Продолжить вывод звука

12

Прервать цикл вывода звука

Теперь поговорим о каждой из этих функций в деталях.
Функция 0: Получить версию драйвера
Эта функция возвращает номер версии драйвера от него самого.
Вход:   ВХ = 0
Выход: АН =старшая часть номера версии.         
AL = младшая часть номера версии.
Функция 1: Установить базовый адрес ввода/вывода
Функция устанавливает базовый адрес ввода/вывода, используемый драйвером. Вы должны задать адрес, который соответствует установленному для платы порту ввода/вывода. Если вы не вызываете эту функцию, драйвер по умолчанию использует адрес 220h.
Вход:     ВХ = 1
AX = базовый адрес ввода/вывода.
Выход: Ничего.
Функция 2: Установить прерывание DMA
Функция устанавливает номер прерывания драйвера прямого доступа к памяти, который используется Sound Blaster для сообщения о завершении передачи данных.
Вход:  ВХ=2
           АХ = Номер прерывания драйвера прямого доступа к памяти.
Выход: Ничего.
Функция 3: Инициализировать драйвер
Эта функция инициализирует и начинает работу драйвера.
Вход:   ВХ=3
Выход: АХ           = 0 - драйвер успешно инициализирован;
= 1 - некорректная версия драйвера;
= 2 -ошибка операции чтения/записи устройством ввода/вывода;
= 3 - ошибка прерывания драйвера прямого доступа к памяти.
Функция 4: Включить/выключить динамик (только для базовой модели Sound Blaster)
Функция включает или выключает вывод звука.
Вход:     ВХ          = 4
AL          = 0 - выключить;
= 1 - включить.
Выход: Ничего.
Функция 5: Установить адрес слова состояния
Функция сообщает драйверу адрес переменной, в которой будет храниться информация О состоянии.


Вы определяете переменную и затем передаете функции ее сегмент и смещение, таким образом, драйвер получает возможность сообщать вашей программе важную информацию о своих параметрах состояния.
Вход:     ВХ=5                
ES:DI = сегмент и смещение переменной, которую вы хотите использовать для хранения слова состояния.
Выход: Ничего.
Функция 6: Начать вывод звука
Это та самая функция, которая все и делает. Она проигрывает VOC-файл, используя прямой доступ к памяти. Однако мы должны передать ей указатель на звуковые данные.
Вход:     ВХ = 6
ES:DI = сегмент и смещение VOC-файла, который вы хотите проигрывать; учтите, что надо быть внимательным и передать функции адрес начала собственно звуковых данных (то есть пропустить информационный заголовок файла; подробнее мы  это рассмотрим ниже).
Выход: АХ = 0 - успешное выполнение операции, в противном случае возвращается ненулевое значение.
Функция 8: Остановить вывод звука
Функция останавливает все операции ввода/вывода и сбрасывает переменную CT_VOICE_STATUS в ноль.
Вход:   ВХ=8
Выход: Ничего.
Функция 9: Закончить работу драйвера
Функция заканчивает работу драйвера. Однако она не выгружает драйвер из памяти - вы должны сделать это самостоятельно.
Вход:  ВХ=9
Выход: Ничего,
Функция 10: Приостановить вывод звука
Функция приостанавливает вывод звука. Вы можете вновь запустить звук, использовав функцию 11 «Продолжить вывод звука».
Вход:     ВХ=10
Выход:  АХ = 0 - успешное выполнение операции;
                      = 1 - вывод звука не происходил.
Функция 11: Продолжить вывод звука
Функция продолжает приостановленный вывод звука.
Вход:     ВХ=11
Выход; АХ = 0 - успешное выполнение операции;
                     = 1 - вывод звука не прерывался.
Функция 12: Прервать цикл вывода звука
Функция прерывает цикл вывода оцифрованных данных.
Вход:     ВХ          = 12
АХ          = 0 - прервать цикл в конце текущей итерации;
= 1 - прервать цикл немедленно.
Выход: АХ           = 0 - успешное выполнение операции;
= 1 - цикл вывода звука не активен.

Определение понятия инструментального средства


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

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

§          Редактор битовых изображений;               

§          Некоторое программное обеспечение для оживления битовых образов;

§          Звуковой АЦП с множеством операций;

§          Программа, для рисования уровней игры;и размещения объектов в игровом пространстве.

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

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


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

Посмотрите внимательно на игру Net-Tank (мы написали ее в предыдущей главе). Вы можете заметить, что игровое поле (поле боя) в ней является двухмерной матрицей элементов, которые транслируются в обычный битовых массив, заключающий в себе саму игру. Игровое поле в Net-Tank настолько просто, что текстового редактора вполне достаточно для рисования уровня. Однако было бы очень мило иметь более совершенный инструмент, с помощью которого можно было бы рисовать поле боя и при этом сразу видеть результат. В случае с Net-Tank, на создание уровня ушло около десяти минут. Однако в играх подобных Wolfeflstein, DOOM или в нашей собственной игре Warlock давать уровни вручную немыслимо, поэтому здесь уже не обойтись без иструмента редактирования уровней.

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

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


Определение значения наклона


Теперь нам надо вычислить каждый из элементов таблицы наклонов. Это должны быть действительные значения наклонов всех возможных линий, отсекаемых от точки наблюдения игрока. Поскольку мы разбили окружность на 1920 секторов, то каждый из них будет по 360/1920 = 0.1875 градусов. Таким образом, мы нашли способ вычисления наклона для всех линий окружности с шагом в 0.1875 градуса. Все это выглядит довольно сложным для вычисления. К счастью, проблему поможет решить функция tg (). Если вы забыли определение тангенса, то напомним, что для прямоугольного треугольника он равен отношению синуса к косинусу угла:

            SinQ          противоположная_сторона

TgQ = --------   =   ------------------------------------

             CosQ          прилегающая_сторона

 

Если это так, то

М = tg q

Создав таблицу значений наклонов из 1920 элементов, где каждый наклон равен tg q, все остальные расчеты значительно упрощаются. Мы используем эти наклоны для построения лучей. Но есть несколько проблем:

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

§          Функция tg() имеет вертикальные асимптоты при углах в 90 и 270 градусов. Поэтому надо быть внимательным, чтобы избежать в этих точках деления на ноль или ошибок с плавающей запятой.                 



Оптимизация отсекателя лучей


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

§          Первое и обязательное условие — это использование целочисленной арифметики вместо операций с плавающей запятой. Это позволит увеличить скорость примерно в 2-4 раза;

§          Во-вторых, мы можем многое оптимизировать в самом тексте программы на Си. Это поднимет быстродействие примерно на 20-50 процентов;

§          Далее, мы можем разбить Х- и Y-отсечения на две независимые программы. Это позволит производить вычисления параллельно;

§          И, наконец, никакая нормальная программа не станет использовать графическую библиотеку Microsoft или разрешение 640х480.

Если использовать уже написанные функции для режима 13Ь, то выполнение программы ускорится в 10 раз и она начнет работать со скоростью 30 кадров в секунду. Но даже если скорость окажется около 15 кадров в секунду, то я буду рад.



Оптимизированные версии OpaqueBlt() и TransparentBlt()


Листинг 17.4 содержит оптимизированные ассемблерные версии подпрограмм OpaqueBlt() и TransparentBlt(), которые имеются и на дискете (BLIT.ASM). Эти подпрограммы можно использовать вместо соответствующих функций на Си, что увеличит быстродействие программы примерно на 30 процентов.

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

Листинг 17.4. Оптимизированная подпрограмма (BLIT.ASM).

ideal

model compact,с

p286

dataseg

VIEW_WIDTH equ 320

VIEW_HEIGHT equ 100

TRANSPARENT equ 0

global MemBuf:dword

.codeseg

public OpaqueBIt public TransparentBIt

;

;Эта процедура копирует битовую карту в MemBuf. Кроме этого, она

; может скроллировать левую и правую части битового образа

; в зависимости от значения ScrollSplit ;

proc OpaqueBlt

ARG Bitmap:dword,StartY:word,Height:word,ScrollSplit:word

USES si,di

les   di,[MemBuf]      ; получить указатель на буфер памяти

mov   ax,[StartY]      ;получить начальную Y-координату

mov   bx,ax            ;скопировать

sal   ax,6             ; умножить на 64

sal   bx,8             ; умножить на 256

add   ax,bx            ; результат равен умножению на 320

add   di,ax            ; вычислить смещение внутри MemBuf

mov   bx,[Height]      ; получить высоту битовой карты

mov   dx,[ScrollSplit] ; получить длину правой половины

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

lds   si,[Bitmap]     ; получить полный указатель на битовую карту

mov   ax,VIEW_WIDTH    ; получить ширину экрана

sub   ax,dx            ; вычислить длину левой половины

cld        ; обрабатывать данные от младших адресов к старшим

@@lоор01: add   di,dx            ; вычислить начальную позицию

mov   сх,ах            ; получить длину левой половины

shr   cx,1 ; разделить на 2 (поскольку выводим по 2 байта)   :




rep   movsw            ; нарисовать правую половину карты  

jnc   short @@skip01   ; пропустить, если выполнено

movsb                  ; дорисовать последний пиксель

@@skip01:

sub   di,VIEW_WIDTH    ; получить ширину выводимого окна

mov   cx,dx            ; получить длину правой половины

shr  сх,1             ; разделить на 2

rep   movsw            ; нарисовать правую половину карты

jnc   short @@skip02   ; пропустить, если выполнено

movsb                  ; перейти к последнему пикселю

@@skip02:

add   di,ax         ; получить ширину следующей строки вывода

dec   bx               ; уменьшить счетчик столбцов

jnz   short @@loop01   ; повторить

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

ret                    ; завершить

endp OpaqueBlt

;

; Эта процедура копирует битовую карту в MemBuf. Функция не рисует

; пиксель, если его цвет равен "прозрачному"

;

proc TransparentBIt

ARG Bitmap:dword,StartY:word,Height:word,ScrollSplit:word USES si,di

les   di,[MemBuf]      ; получить указатель на память

mov   ax,[StartY]      ; получить начальную Y-координату

mov   ex, ax            ; получить копию

sal   ax,б             ; умножить на 64

sal   ex,8             ; умножить 256

add   ax, ex          ; результат эквивалентен умножению на 320

add   di,ax            ; прибавить смещение к MemBuf

mov   dx,[ScrollSplit] ; получить ширину левой половины

                        ;  изображения

mov   bx,VIEW_WIDTH    ; общая ширина изображений

sub   bx,dx            ; вычислить ширину правой половины

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

lds   si,[Bitmap] ; получить полный указатель на битовый образ

@@lоор01;

add   di,dx            ; вычислить начальную позицию экрана

mov   сх,Ьх            ; получить ширину правой половины ;

;Нарисовать правую половину ;

@@lоор02;

mov   аl,[si]            ; получить пиксель карты

inc   si                 ; следующий пиксель

cmp   al,TRANSPARENT     ; "прозрачный"?

je    short @@skip01     ; ...пропустить, если да



mov   [es:di],al         ; иначе, нарисовать его

@@skip01:

inc   di                 ; указать на следующий байт MemBuf

dec   ex                 ; уменьшить счетчик

jnz   short @@loop02                

sub   di,VIEW_WIDTH       ; получить ширину выводимого окна

mov   cx,dx               ; получить ширину правой половины

; Нарисовать левую половину

@@lоор03:

mov   ai,[si]            ; получить пиксель карты

inc   si                 ; следующий пиксель

cmp   al,TRANSPARENT     ; "прозрачный"?

je    short @@skip02       ;...пропустить, если да

mov   [es:di],al           ;иначе, нарисовать его

@@skip02;

inc   di                   ;перейти к следующему байту видеобуфера

dec   ex                   ;уменьшить счетчик

jnz   short @@loop03       ; повторить цикл, пока не сделаем все

add   di,bx                ;добавить значение ширины правой

части изображения

dec   [Height]             ;уменьшить счетчик колонок

jnz   short @@loop01       ;повторить еще раз

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

ret                       ; закончить

endp TransparentBlt

end


ОСНОВЫ ЯЗЫКА АССЕМБЛЕРА


Доктор Руди Раккер, мой друг и бывший наставник, как-то сказал: «Ассемблер - это просто круто». Я думаю, эти слова как нельзя лучше подходят к данной главе. Ассемблер — это родной язык всех компьютеров, и если вы им хорошо овладеете, он предоставит вам фантастические возможности. В настоящей главе мы кратко ознакомимся с этим языком.

Мы научимся подключать фрагменты, написанные на ассемблере, к нашим программам и использовать встроенный (in-line) ассемблер компилятора Microsoft С. Кроме того, мы пойдем чуть дальше и напишем еще парочку графических процедур. Таким образом, эту главу можно разделить на следующие части:

§          Зачем нам нужен ассемблер при написании игр;

§          Описание семейства процессоров 80х86;

§          Регистры ЦПУ;

§          Общий вид процедуры на ассемблере;

§          Передача параметров;

§          Локальные переменные;

§          Создание внешних ссылок;

§          Возвращение параметров;

§          Некоторые полезные управляющие конструкции;

§          Установка видеорежимов;

§          Сверхскоростная очистка экрана;

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



Основы контроля столкновений


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

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

1.

Забыть о реальных границах астероида и корабля и создать виртуальную граничную область вокруг каждого объекта исключительно для тестовых целей. Не надо ее рисовать, достаточно, чтобы она присутствовала внутри программы. Рисунок 4.10 показывает, что я имею в виду.

2.       Если мы увидим, что граничная область нашего судна вошла в граничную область астероида, мы можем сказать, что наш корабль уничтожен. Это довольно просто сделать: достаточно сравнить каждую вершину области с вершиной другой области. Если есть вхождение, значит произошло столкновение.

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

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

Кроме этого, нам надо набирать очки. Значит, нужно иметь оружие. Например, корабль в игре может использовать фотонные торпеды — маленькие точки. Чтобы их получить, нам надо:

1.       Назначить одну из клавиш (лучше Пробел) для стрельбы.

2.       Когда будет обнаружено нажатие на клавишу, определить несколько переменных, в которых запомнить направление движения корабля (miss_xv и miss_yv).

3.       Затем запомнить в двух переменных исходную позицию ракеты, то есть координаты центра корабля (miss_x и miss_y).

4.       Присвоить переменной life значение 100.

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

6.        Уменьшить значение переменной life, и если она стала равна нулю, то уничтожить ракету и дать возможность игроку выстрелить следующий раз.



ОСНОВЫ РАБОТЫ С УСТРОЙСТВАМИ ВВОДА


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

§          Взаимодействие с пользователем в видеоиграх;

§          Джойстик и работа с ним;

§          Работа с клавиатурой;

§          Управление мышью.



Особые ситуации


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

Переходы от одного кирпичика к другому

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

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

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

Давайте посмотрим, как создать переходный «кирпичик» между изображением кустарника и элементом стены пещеры, выложенной валунами. Элементы пейзажа, для которых мы собираемся создать переходный «кирпичик», изображены на рисунке 16.21. Как видите, каждый «кирпичик» сам по себе является бесшовным, то есть они прекрасно соединяются каждый с самим собой, но как только один из них сменится другим, шов станет очень заметен (рис, 16.22). Чтобы избежать такого контраста, мы и должны нарисовать «кирпичик», сглаживающий переход.

Идея переходных «кирпичиков» состоит в том, что с одного края он плавно переходит в один «кирпичик», а с противоположной стороны — в другой. Скажем, вы хотите создать промежуточный «кирпичик», связывающий элементы кустарника и пещеры. Для этого нужно выполнить следующие шаги:

1. Разбейте обе картинки точно пополам. Затем возьмите левую половину изображения кустарника и соедините ее с правой частью «кирпичика» пещеры.


Теперь левый край полученного рисунка состыкуется без шва с «кирпичиком» кустарника, а правый — с «кирпичиком» пещеры (рисунок 16.23).

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



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



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

Двери и лифты

Раздвижные двери и двери лифтов состоят из двух «кирпичиков»: собственно Двери и косяка. Для этого примера возьмем два «кирпичика», которые были





созданы, когда изготовлялась область панели управления. Эти два изображения исключительно хорошо подходят для двери лифта. «Кирпичик» справа безупречно подходит для косяка, а кирпичик слева будет прекрасен для двери лифта (это показано на рисунке 16.26).

Надо подумать о «кирпичике», который будет представлять изображение В проеме открывающейся двери. Чтобы сделать Косяк и дверь из этих «кирпичиков» надо:

1. Нарисовать темный прямоугольник в центре «кирпичика»;

2. Немного смягчить и подровнять границы, чтобы линии не выглядели отрывистыми;

3. Добавить небольшие детали, такие как светодиоды или кнопки. Результат показан на рисунке 16.27.



Вам необходимо иметь два «кирпичика» для двери лифта;

• Первый «кирпичик» предназначен для двери, которая сдвигается влево или вправо. Кнопки на этом «кирпичике» расположены на левом краю: сторона двери, которая будет видимой, пока дверь уберется в стену (как показано на рисунке 16.28);



• Второй дверной «кирпичик» имеет кнопки на правой стороне двери. Открываясь, эта дверь будет сдвигаться справа налево.

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



Специальные эффекты и движущиеся стены

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

Один из «кирпичиков» панели управления мы уже изготовили ранее и теперь можем попытаться оживить его. Давайте посмотрим, что для этого нужно сделать. Взгляните на следующие шесть элементов, три из которых показаны на рисунке 16.29, а остальные — на рисунке 16.30. Заметьте, что каждый «кирпичик» слегка отличается от других. Некоторые светодиоды, зажженные на одной панели, погашены в других местах, различаются также и цифры на дисплее, и синяя линия осциллограммы. На последних трех «кирпичиках» выключатель изменяет свое положение от верхнего до нижнего, что может быть использовано для изображения работающей или отключенной панели управления.



Будучи оживленными, эти «кирпичики» выглядят так, словно панель управления работает: хаотично вспыхивают светодиоды, что-то мелькает на дисплее. Чтобы изготовить такие «кирпичики», достаточно сделать несколько копий исходного изображения, а затем слегка модифицировать каждую из них.

Удаление неровностей

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

Часто помогает выбор в качестве фонового цвета для спрайта того тона который преобладает в изображении, с последующим сглаживанием между основным рисунком и фоном.Затем оставшийся фон может быть заменен на «прозрачный». Это сложный процесс, но по мере накопления опыта он станет проще.

Как избежать цветовых проблем во время игры

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


Освещение, тени и палитра


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

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

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



Освещение ваших игр


Освещение, тени и другие эффекты традиционно выполняются с помощью сложной математики, основанной на физических моделях. При построении этих моделей принимаются во внимание такие факторы как угол падения, расстояниеИ до источника света, тип источника света, тип материала, плотность атмосферы  и т. д. (более подробно об этом рассказывается в шестой главе, «Третье измерение». Не удивительно, что для получения фотографически точных изображений тратятся часы или даже дни работы суперкомпьютеров типа Cray ХМР! Уравнения, которые надо для этого решить, да и количество операций, которое необходимо выполнить — все это слишком грандиозно для нашего маленького персонального компьютера. Однако мы попытаемся, используя смекалку и упрощенные модели освещения, реализовать несколько «достаточно хороших» для наших потребностей эффектов. Потрясающие световые эффекты знаменитой игры DOOM впечатляют, по, тем не менее, в них нет ничего невозможного. Как говорится, глаза боятся, а руки делают.

 В качестве примера представим, что мы нарисовали комнату, используя регистры цвета от 1 до 32. Эти регистры содержат оттенки серого (вместе с  оттенками красного, которыми изображены ругательства, написанные на стенах). Что произойдет, если мы постепенно увеличим значение RGB в каждом  из этих регистров? Вы увидите, что комната стала светлее. Если же вы уменьшите каждое значение RGB во всех регистрах цвета, в комнате станет темнее.

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




Для примера, давайте будем уменьшать каждое значение RGB на 5 процен­тов за каждую итерацию. Чтобы сделать это, мы должны вычислить 5 процентов для каждого из значений RGB и вычесть их из каждого компонента. Таблица 7.2 показывает результаты этой процедуры за небольшой промежуток времени для некоторого произвольного цвета RGB.

Таблица 7.2. Уменьшение интенсивности цвета при сохранении цветового баланса.

(Цветовой баланс с использованием красного цвета в качестве эталона)

R

G

B

Цветовой баланс

Итерация 1

60

40

20

1:0.66:0.30

Итерация 2

57

38

19

1:0.66:0.33

Итерация 3

54

36

19

1:0.660.33

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

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

R

G

B

Цветовой баланс

55

37

19

1:0.67:0.34

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


Откуда берутся идеи?


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

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



Отображение текста


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

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

Данные для таблицы символов ROM размером 8х8 начинаются с адреса F000:FA6E. Символы располагаются в ASCII-последоватедьности, то есть описание символа А находится на 65-м месте и т. д. Каждый символ состоит из восьми байт, где каждый байт представляет собой одну строку битовой карты символа. Мы можем умножить 65 на 8, чтобы найти смещение первого байта данных в наборе символов. Это смещение затем складывается с базовым адресом FOOO:FA6E, и следующие восемь байт представляют каждую из строк символа. Давайте посмотрим на рисунок 5.13 для большей ясности.

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

Алгоритм выглядит примерно так:

§          Получить следующую строку данных  символа (следующий байт);

§          Нарисовать по горизонтали восемь точек, где пиксели с 0 по 7 установлены либо в 1, либо в 0, в зависимости от значения битов 0 - 7 в байте данных;

§          Проделать то же самое для каждой строки данных символа (байта).



Отрисовка дверей и прозрачных областей


Я надеюсь, вы видели, как открываются двери в Wolfenstein'e и DOOM'e. Когда они открыты, вы можете заглянуть в соседнюю комнату. С точки зрения трассировки лучей, при этом возникает множество проблем. С одной стороны, Дверь должна быть нарисована. С другой — должно быть нарисовано и то, что находится позади нее. Для решения этой проблемы луч трассируется до пересечения с дверью и затем проходит сквозь нее.

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

Это достигается следующим образом:

1. В процессе трассировки луч достигает сплошной стены.

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

3.       Запоминается полученное пересечение.

Этот процесс может продолжаться до бесконечности, но сейчас давайте предположим, что он прекращается после пересечения второй стены. Как показано на рисунке 6.31, мы получаем два пересечения.

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

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