Источник(битовый образ)
|
Пример обработчика прерывания № 1 - Там полно звезд...
Чтобы продемонстрировать вам, насколько полезными могут оказаться многозадачность и прерывания, я написал в виде обработчика прерывания программу, которая рисует трехмерное звездное небо. Как вам уже известно, при каждом приращении счетчика внутреннего таймера, генерируется прерывание. При помощи вектора 0х1С я сопоставил этому прерыванию свою процедуру обслуживания, которая и создает трехмерное звездное небо. Поскольку обращение к моему обработчику происходит 18.2 раза в секунду, значит и картина звездного неба обновляется с этой же частотой. Нечто подобное мы могли бы использовать при написании игры, работающей на одном экране (игры типа «перестреляй их всех»). Наше звездное небо будет изменяться независимо от того, что будет происходить на переднем плане.
Обратите внимание на то, что процедура обслуживания прерывания сделана автономной. Это означает, что она инициализирует свои данные при первом обращении к ней (в этот момент она создает базу данных с информацией о звездах), а при последующих вызовах просто видоизменяет картинку.
И напоследок еще одно маленькое замечание: завершая программу, вы можете оставить ее резидентной в памяти, нажав клавишу Е. Сделав это, вы увидите, что звездное небо присутствует в строке приглашения DOS, что выглядит несколько странно! Причина такого поведения программы кроется в том, что когда вы завершаете ее нажатием клавиши Е, она не восстанавливает прежний обработчик прерывания. Компьютер продолжает вызывать по прерыванию от таймера наш обработчик. Вы наверняка даже и представить себе не могли, что это сработает! И, тем не менее, завершаясь, программа оставляет в оперативной памяти большую часть своего кода неизменным, в результате чего обработчику прерывания удается пережить окончание работы породившей его программы. Если вы попытаетесь запустить еще какую-нибудь программу, компьютер, скорее всего, «зависнет». Поэтому для написания настоящих резидентных программ такой метод применять не стоит. (На самом деле для этих целей предназначена специальная функция DOS, которая называется dos keep, но сейчас мы не будем подробно рассматривать резидентные программы.
Мы просто случайно создали одну из них). Если система «зависнет», вам придется перезагрузиться.
Текст программы, изображающей трехмерное звездное небо, представлен в Листинге 12.6.
Листинг 12.6. Трехмерное звездное небо (STARS.C).
// ВКЛЮЧАЕМЫЕ ФАЙЛЫ /////////////////////////////////////
#include
#include
#include
#include
#include
#include
// ОПРЕДЕЛЕНИЯ //////////////////////////////////////////
#define TIME_KEEPER_INT 0x1C
#define NUM_STARS 50
// структуры ////////////////////////////////////////////
typedef struct star_typ
int x,y; // координаты звезды
int vel; // проекция скорости звезды на ось Х
int color; // цвет звезды
} star, *star_ptr;
// ГЛОБАЛЬНЫЕ ПЕРЕМЕННЫЕ /////////////////////////////////////////
void (_interrupt _far *01d_Isr)(void);
// хранит старый обработчик прерывания
unsigned char far *video_buffer = (char far *)0xA0000000L;
// указатель на видеобуфер
int star_first=1; // флаг первого вызова автономной функции
// звездного неба
star starstNUM_STARS]; //звездное небо
// функции ///////////////////////////////////////////////////////
void Plot_Pixel_Fast(int x,int y,unsigned char color)
{
// эта функция рисует точку заданного цвета несколько быстрее,
// чем обычно за счет применения операции сдвига вместо операции
// умножения
// используем известный факт, что 320*у = 256*у + 64*у = у<<8 + у<<6
video_buffer[((у<<8) + (у<<6)) + х] = color;
} // конец Plot Pixel_Fast
/////////////////////////////////////////////////////////////
void _interrupt _far Star_Int(void)
{
// эта функция создает иллюзию трехмерного звездного неба,
// наблюдаемого из иллюминатора космического корабля Enterprise
// Замечание: эту функцию следует исполнять не реже одного раза
// в 55.4мс, иначе не избежать перезагрузки!
int index;
// проверка, надо ли нам инициализировать звездное поле
// (то есть первый ли раз вызывается функция)
if (star_first)
{
// сброс флага первого вызова
star_first=0;
// инициализация всех звезд
for(index=0; index
{
// инициализация цвета, скорости и координаты каждой звезды
stars[index].х = rand()%320;
stars[index].у = rand()%180;
// выбор плоскости для звезды switch(rand()%3)
{ case 0:
// плоскость 1 - наиболее удаленная плоскость
{
// установка скорости и цвета звезды
stars[index].vel = 2;
stars[index].color = 8;
} break;
case 1: // плоскость 2 - плоскость, расположенная
// посередине
{
stars[index].vel = 4;
stars [index] .color =7;
} break;
case 2: // плоскость 3 -самая ближняя плоскость
{
stars[index].vel = 6;
stars[index].color = 15;
} break;
} // конец оператора switch
} // конец цикла
} // конец оператора if
else
{ // не первый вызов функции, поэтому производим рутинные
// действия: стирание, перемещение, рисование
for (index=0; index
{ // стирание
Plot_Pixel_Fast(stars[index].х,stars[index]-у,0);
// перемещение
if ((stars[index].x+=stars[index].vel) >=320 ) stars[index].х = 0;
// рисование
Plot_Pixel_Fast(stars[index],x,stars[index],y, stars[index].color);
} // конец цикла
} // конец else
} // конец Star_Int
// ОСНОВНАЯ ПРОГРАММА ///////////////////////////////////
void main(void)
{ int num1, num2,с
;
_setvideomode(_MRES256COLOR) ;
// установка обработчика прерывания
Old_Isr = _dos_getvect(TIME_KEEPER_INT) ;
_dos_setvect(TIME_KEEPER_INT, Star_Int);
// ожидание нажатия клавиши пользователем
_settextposition(23,0);
printf("Hit Q - to quit.");
printf("\nHit E - to see something wonderful...");
// чтение символа
с = getch();
// хочет ли пользователь рискнуть?
if (c=='e')
{
printf("\nLook stars in DOS, how can this be ?") ;
exit(0);
// выход без восстановления старого обработчика прерывания
} // конец оператора if
/ восстановление старого обработчика прерывания
_dos_setvect(TIME_KEEPER_INT, 01d_Isr);
_setvideomode(_DEFAULTMODE) ; }// конец
функции main
Пример обработчика прерывания № 2 - Ловим нажатия клавиш!
Как мы узнали в третьей главе, «Основы работы с устройствами ввода», мы можем использовать BIOS для чтения нажатия клавиши путем проверки значения скан-кода. Это отличный способ, и он вполне применим для большинства случаев, но что делать, если вы хотите одновременно нажать две клавиши? (Примером такой ситуации может служить момент, когда вы одновременно нажимаете клавиши «стрелка вверх» или «стрелка вниз» и «стрелка вправо» чтобы изменить направление движения на диагональное). Единственный способ обработать подобную ситуацию в программе требует более тонкой работы с клавиатурой. BIOS по сравнению со стандартными функциями Си просто дает нам еще один уровень управления, но если мы хотим добиться необходимой для профессионально написанных компьютерных игр функциональности, нам следует глубже, глубже и еще глубже разобраться с клавиатурой.
На самом деле клавиатура персонального компьютера представляет собой отдельный микрокомпьютер, который называется 8048. При нажатии или отпускании клавиши клавиатура посылает последовательность сигналов персональному компьютеру на обработку. BIOS сообщает нам о том, какая клавиша нажата, но умалчивает об отпущенных клавишах. Такой половинчатой информации не достаточно для того, чтобы организовать обработку одновременного нажатия нескольких клавиш. Нам нужно знать и когда была нажата, и когда была отпущена та или иная клавиша, чтобы написать алгоритм, способный отслеживать состояния некоторого набора клавиш. При нажатии клавиши мы выставляем соответствующий ей флаг в еостояние «нажато», а при отпускании — сбрасываем значение этого флага. Применяя такой подход, мы регистрируем нажатия различных клавиш и храним эту информацию в нашей структуре данных до тех пор, пока клавиша не будет отпущена.
При нажатии клавиши клавиатурой посылается код нажатия, а при отпускании — соответствующий код отпускания. Эти коды похожи на уже известный вам скан-код. Различие состоит в том, что при отпускании клавиши, каким бы ни было значение кода нажатия, к нему всегда прибавляется 128.
Чтобы наша программа смогла взять контроль за клавиатурой на себя, мы выгрузим обработчик клавиатуры операционной системы DOS и установим наш собственный драйвер. Он будет получать коды клавиш независимо от того, что произошло — нажатие или отпускание, а затем сохранять эту информацию в глобальной переменной, к которой имеет доступ наша Си-программа. При таком подходе функция нашей программы сможет использовать текущее значение этой переменной для выяснения того, какие клавиши в данный момент нажаты, а какие отпущены. Разобравшись что к чему, функция сведет эту информацию в таблицу. Клавиатурное прерывание имеет номер 0х09. Все, что нам требуется сделать, это написать и установить соответствующую процедуру обработки данного прерывания.
Прежде чем мы этим займемся, вспомним адреса клавиатурных портов ввода/вывода и их функции. Собственно клавиатурный порт ввода/вывода находится по адресу 60h, а регистр, управляющий клавиатурой — по адресу 61lh Эти порты находятся на микросхеме PIA (Peripheral Interface Adapter). Кроме всего прочего, нам еще потребуется выполнить определенные процедуры перед вызовом нашего обработчика прерывания и после его завершения. Общий порядок всех действий таков:
1.
Войти в процедуру обслуживания прерывания. Это происходит при каяждом нажатии клавиши.
2. Прочитать из порта ввода/вывода 60h код клавиши и поместить его в глобальную переменную для последующей обработки программой или обновления содержимого таблицы, в которой хранится информация о нажатых клавишах.
3. Прочитать содержимое управляющего регистра из порта ввода/вывода 61h и выполнить над ним логическую операцию OR с числом 82h.
4. Записать полученный результат в порт регистра управления 61h.
5. Выполнить над содержимым управляющего регистра логическую операцию AND с числом 7Fh. Это сбрасывает состояние клавиатуры, давая ей понять что нажатие на клавишу обработано и мы готовы к считыванию информации о нажатии других клавиш.
6. Сбросить состояние контроллера прерываний 8259. (Без этого можно и обойтись, однако лучше подстраховаться). Для этого следует записать в порт 20h число 20h. Забавное совпадение, не правда ли?
7. Выйти из обработчика прерывания.
Листинг 12.7 содержит текст программы обработчика клавиатурного прерывания, позволяющего отслеживать состояние клавиш со стрелками. При работе этой программы вы можете использовать курсорные клавиши или их комбинации для перемещения маленькой точки в любом направлении по экрану.
Листинг 12.7. Киберточка (CYBER.C)._____________________
// ВКЛЮЧАЕМЫЕ ФАЙЛЫ///////////////////////////////////////
#include
#include
#include
#include
#include
#include
// ОПРЕДЕЛЕНИЯ //////////////////////////////////////////
#define KEYBOARD_INT 0х09
#define KEY_BUFFER 0х60
#define KEY_CONTROL 0х61
#define INT_CONTROL 0х20
// коды нажатия и отпускания для клавиш со стрелками
#define MAKE_RIGHT 77
#define MAKE_LEFT 75
#define MAKE_UP 72
#define MAKE_DOWN 80
#define BREAK__RIGHT 205
#define BREAK_LEFT 203
#define BREAK_UP 200
#define BREAK_DOWN 208
// индексы в таблице состояния клавиш со стрелками
#define INDEX_UP 0
#define INDEX_DOWN 1
#define INDEX_RIGHT 2
#define INDEX_LEFT 3
// ГЛОБАЛЬНЫЕ ПЕРЕМЕННЫЕ ////////////////////////////////
void (_interrupt _far *01d_Isr)(void);
// хранит старый обработчик прерывания
unsigned char far *video_buffer = (char far *)0xA0000000L;
// указатель на видеобуфер
int raw_key; // необработанные данные от клавиатуры
int key_table[4] = {0,0,0,0};
// таблица состояний клавиш со стрелками
// ФУНКЦИИ //////////////////////////////////////////////
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 Fun_Back(void)
{
int index;
// несомненно запоминающийся рисунок фона
_setcolor(1) ;
_rectangle(_GFILLINTERIOR, 0,0,320,200);
_setcolor{15) ;
for (index=0; index<10; index++)
{
_moveto(16+index*32,0);
_lineto(16+index*32,199);
} // конец
цикла
for (index=0; index<10; index++)
{
_moveto(0,10+index*20) ;
_lineto(319,10+index*20);
} // конец цикла
} // конец Fun Back
///////////////////////////////////////
void _interrupt _far New_Key_Int(void)
{
// я в настроении немного попрограммировать на ассемблере!
_asm{
sti ; разрешаем прерывания
in al,KEY BUFFER ; получаем нажатую клавишу
xor ah,ah ; обнуляем старшие 8 бит регистра АХ
mov raw_key, ax ; сохраняем код клавиши
in al,KEY_CONTROL ; читаем управляющий регистр
or al,82h ; устанавливаем необходимые биты для сброса FF
out KEY_CONTROL,al ; посылаем новые данные в управляющий регистр
and al,7fh
out KEY_CONTROL,al ; завершаем
сброс
mov al,20h
out INT CONTROL,al; завершаем
прерывание
} // конец ассемблерной вставки
// теперь вернемся К Си, чтобы изменить данные
// в таблице состояния клавиш со стрелками
// обработка нажатой клавиши и изменение таблицы
switch(raw_key)
{
case MAKE_UP: // нажатие стрелки вверх
{
key_table[INDEX_UP] = 1;
} break;
case MAKE_DOWN: // нажатие стрелки вниз
{
key_table[INDEX_DOWN] = 1;
) break;
case MAKE_RIGHT: // нажатие' стрелки вправо
{
key_table[INDEX_RIGHT] = 1;
} break;
case MAKE_LEFT: // нажатие стрелки влево
{
key__table[INDEX_LEFT] = 1;
} break;
case BREAK_UP: // отпускание стрелки вверх
{
key_table[INDEX_UP] = 0;
} break;
case BREAK DOWN: // отпускание стрелки вниз
{
key_table[INDEX_DOWN] = 0;
} break;
case BREAK_RIGHT; // отпускание стрелки вправо
{
key_table[INDEX_RIGHT] = 0;
} break;
case BREAK_LEFT: // отпускание стрелки влево
{
key_table[INDEX_LEFT] = 0;
} break;
default: break;
} // конец оператора switch
} // конец New_Key_Int
// ОСНОВНАЯ ПРОГРАММА ////////////////////////////////
void main(void)
{
int dопе=0, x=160, y=100;// флаг выхода и координаты точки
//установка видеорежима 320x200x256
_setvideomode(_MRES256COLOR) ;
Fun_Back(); // оригинальная картинка, не так ли?
printf("\nPress ESC to Exit.");
// установка нового обработчика прерывания
Old_Isr = _dos_getvect(KEYBOARD_INT) ;
_dos_setvect(KEYBOARD_INT, New_Key_Int);
// основной цикл
while(!done)
{
_settextposition(24,2) ;
printf("raw key=%d ",raw_key);
// смотрим в таблицу и перемещаем маленькую точку
if (key_table[INDEX_RIGHT]) х++;
if (key_table[INDEX_LEFT]) х--;
if (key_table[INDEX_UP]) y--;
if (key_table[INDEX_DOWN]) y++;
// рисуем
киберточку
Plot_Pixel_Fast(x,y,10);
// Наша клавиша завершения. Значение кода нажатия ESC равно 1
if (raw_key==1) done=l;
} // конец while
// восстановление старого обработчика прерывания
_dos_setvect(KEYBOARD_INT, Old_Isr) ;
_setvideomode (_DEFAULTMODE) ;
} // конец
функции main
Уфф... Вот и все, ребята!
Пробовали ли вы текстурировать?
Так как основная тема всех наших разговоров — это то, как сделать трехмерную игру типа DOOM с текстурированными поверхностями, то мне следует рассказать вам о несомненно важной вещи, которую мы еще не обсуждали — об этом самом тексту рировании. Все эти темные комнаты и туннели просто обязаны иметь действительно потрясающую текстуру. Здесь есть только одна проблема, а что если вы не художник-профессионал? Как же вы их сделаете?
Вы будете приятно удивлены, узнав что достаточно внимательно посмотреть на всевозможные здания и сооружения, а затем посвятить некоторое время программе Deluxe Paint. Взгляните на рис. 7.8.
Эти несколько грубых текстур я сделал с помощью этой программы за несколько минут. Эти текстуры не так уж плохи, к тому же они показывают, что это можно сделать и не будучи профессионалом. Однако если вы рисуете совсем как кура лапой, то можете воспользоваться тысячей уже готовых текстур с одного из многочисленных дисков CD-ROM.
(Однако, пожалуйста, не размещайте водопроводные краны и ванные на стенах, как это сделано в некоторых играх, названия которых вы сами легко вспомните.)
Проекции
Сейчас мы знаем, как изобразить трехмерный объект и как произвести операции перемещения, масштабирования и поворота этого объекта. Возникает вопрос: «А как мы можем рисовать трехмерные объекты на плоском экране?» ответ прост: мы «проецируем» их на поверхность экрана.
Проекция трехмерных объектов на плоскость довольно проста и дает хорошие результаты. К сожалению, образы при этом не всегда выглядят реалистично. Ваши глаза имеют одну точку зрения, а, следовательно, образ будет похож на трехмерный, но не станет «по-настоящему» объемным.
Замечание
Существуют специальные шлемы с дисплеями, дающие стереоскопический эффект. Эта шлемы используются в системах виртуальной реальности. Проблема, возникающая с их использованием, заключается в том, что параллакс или фокальная точка различна для каждого человека. Если шлем не отрегулирован и точка не подобрана, то зритель вскоре получит сильнейшую головную боль.
Тем не менее, мы будем использовать монитор для проецирования трехмерного образа на экран. Я хотел бы обсудить типы проекции, которые могут применяться с этой целью: параллельная, или ортогональная проекция и перспективная проекция. Рисунок 6.5 показывает диаграммы каждого типа проекций.
Параллельная проекция проста в реализации, но образы не выглядят объемными. Они, скорее, похожи на обычные плоские картинки. Для реализа ции такой проекции достаточно убрать Z-компонент каждой точки трехмерного объекта и затем нарисовать объект, как двухмерный.
С другой стороны, перспективная проекция дает большее приближение и выглядит почти «трехмерно». Она имеет качество «длинной дороги». На рисунке 6.6 изображена такая «дорога». Перспективная проекция принимает во внимание 2-компонент и соответственно изменяет компоненты Х и Y.
Элементы, которые подвергаются «перспективному» преобразованию, должны:
§ Просто делиться или умножаться на Z-компонент;
§ Обладать дистанцией просмотра.
Вскоре мы затронем детали проецирования, но сначала давайте поговорим об экране и его взаимоотношениях с координатами трехмерного пространства.
Программа Астероиды с использованием матриц
Я уже устал от разговоров — давайте что-нибудь напишем. К примеру, перепишем нашу программу из Листинга 4.8. Вы должны ее помнить. Мы ее переделаем и используем в ней матрицы. Листинг 4.11 показывает эту программу. Она называется «Супер Астероиды».
Листинг 4.11. Супер Астероиды (FIELD.C).
// ВКЛЮЧАЕМЫЕ ФАЙЛЫ ///////////////////////////////////////////
#include
#include
#include
//ОПРЕДЕЛЕНИЯ ///////////////////////////////////////////////
#define NUM__ASTEROIDS 10
#define ERASE 0
#define DRAW 1
#defineX_COMP 0
#define Y_COMP 1
#define N_COMP 2
// СТРУКТУРЫ ДАННЫХ ////////////////////////////////////
// новая структура, описывающая вершину
typedef struct vertex_typ
{
float p[3]; // единичная точка в двумерном пространстве
//и фактор нормализации
} vertex, *vertex_ptr;
// общая структура матрицы
typedef struct matrix_typ
{
float elem[3] [3]; // массив для хранения элементов
// матрицы
} matrix, *matrix_ptr;
// определение структуры "объект",.
typedef struct object_typ
{
int num_vertices; // количество вершин в объекте
int color; // цвет объекта
float xo,yo; // позиция объекта
float x_velocity; // скорости пермещения по осям Х float y_velocity;
// и Y matrix scale; // матрица масштабирования
matrix rotation; // матрицы поворота и перемещения
vertex vertices[16]; // 16 вершин
} object, *object_ptr;
// ГЛОБАЛЬНЫЕ.ПЕРЕМЕННЫЕ /////////////////////////////////
object asteroids [NUM_ASTEROIDS] ;
// ФУНКЦИИ ////////////////////////////////////////////
void Delay(int t)
{
// функция выполняет некоторую задержку
float x = 1;
while(t—>0)
x=cos(x) ;
} // конец функции
///////////////////////////////////////////////////////
void Make_Identity(matrix_ptr i)
{
// функция формирует единичную матрицу
i->elem[0][0] = i->elem[l][1] = i->elem[2][2] = 1;
i->elem[0][1] = i->elem[l][0] = i->elem[l][2] = 0;
i->elem[2][0] = i->elem[01[2] = i->eiem[2][1] = 0;
} // конец функции ///////////////////////////////////////////////////////
void Clear_Matrix(matrix_ptr m) {
// функция формирует нулевую матрицу
m->е1еm[0][0] = m->elem[l][1] = m->е1еm[2][2] = 0;
m->elem[0][1] = m->elem[l] [0] = m->е1еm[1] [2] = 0;
m->elem[2][0] = m->elem[0] [2] = m->elem[2][1] = 0;
} // конец
функции
///////////////////////////////////////////////////////
void Mat_Mul (vertex_ptr v, matrix_ptr m)
{
// функция выполняет умножение матрицы 1х3 на матрицу 3х3
// элемента. Для скорости каждое действие определяется "вручную",
// без использования циклов. Результат операции - матрица 1х3
float x new, у
new;
x_new=v->p[0]*m->elem[0][0] + v->p[1]*m->elem[1][0] + m->elem[2][0];
y_new=v->p[0]*m->elem[0][1] + v->p[1]*m->elem[1][1] + m->elem[2][1];
// N_COMP - всегда единица
v->p[X_COMP] = x_new;
v->p[Y_COMP] = y_new;
} // конец
функции
///////////////////////////////////////////////////////
void Scale_Object_Mat(object_ptr obj)
{
// функция выполняет масштабирование объекта путем умножения на
// матрицу масштабирования
int index;
for (index=0; indexnum_vertices; index++)
{
Mat_Mul ((vertex_ptr) &obj->vertices [index],
(matrix_ptr)&obj->scale) ;
} // конец цикла for
} // конец функции
///////////////////////////////////////////////////////
Rotate_Object_Mat(object_ptr obj)
{
// функция выполняет.поворот объекта путем умножения на
// матрицу поворота
int index;
for (index=0; indexnum_yertices; index++)
{
Mat_Mul((vertex_ptr)&obj->vertices[index],(matrix_ptr)&obj->rotation) ;
} // конец цикла for
} // конец функции ///////////////////////////////////////////////////////
void Create_Field(void)
{
int index;
float angle,c,s;
// эта функция создает поле астероидов
for (index=0; index
{
asteroids[index].num vertices = 6;
asteroids [index] .color = 1 + rand() % 14; //астероиды
// всегда
видимы
asteroids[index].xo = 41 + rand() % 599;
asteroids[index].yo = 41 + rand() % 439;
asteroids [index].,x_velocity = -10 +rand() % 20;
asteroids[index].y_velocity = -10 + rand() % 20;
// очистить
матрицу
Make_Identity((matrix_ptr)&asteroids[index].rotation) ;
// инициализировать матрицу вращений
angle = (float) (- 50 + (float) (rand ()\ % 100)) / 100;
c=cos(angle);
s=sin(angle);
asteroids[index].rotation.elem[0][0] = с;
asteroids[index].rotation.elem[0][1] = -s;
asteroids[index].rotation.elem[l][0] = s;
asteroids[index].rotation.elem[l][1] = с;
// формируем матрицу масштабирования
// очистить матрицу и установить значения коэффициентов
Make_Identity((matrix ptr)&asteroids[index].scale);
asteroids[index].scale.elem[0][0] = (float) (rand() % 30) / 10;
asteroids[index].scale.elem[1][1] = asteroids[index].scale.elem[0][0];
asteroids[index].vertices[0].p[X_COMP] = 4.0;
asteroids[index].vertices[0].p[Y_COMP] = 3.5;
asteroids[index].vertices[0].p[N_COMP] = l;
asteroids[index].vertices[1].p[X_COMP] = 8.5;
asteroids[index].vertices[l].p[Y_COMP) = -3.0;
asteroids[index].vertices[1].p[N_COMP] = l;
asteroids[index].vertices[2].p[X_COMP] = 6;
asteroids[index].vertices[2].p[Y_COMP] = -5;
asteroids[index].vertices[2].p[N_COMP] = l;
asteroids[index].vertices[3].p[X_COMP] = 2;
asteroids[index].vertices[3].p[Y_COMP] = -3;
asteroids[index].vertices[3].p[N_COMP] = l;
asteroids[index].vertices[4].p[X_COMP] = -4;
asteroids[index].vertices[4].p[Y_COMP] = -6;
asteroids[index].vertices[4].p[N_COMP] = 1;
asteroids[index].vertices[5].p[X_COMP] = -3.5;
asteroids[index].vertices[5].p[Y_COMP] = 5.5;
asteroids[index],vertices[5].p[N_COMP] = 1;
// теперь масштабировать астероиды
Scale_Object_Mat((object_ptr)&asteroids[index]);
} // конец цикла for
} // конец функции
///////////////////////////////////////////////////////
void Draw Asteroids (int erase)
{
int index,vertex;
float xo,yo;
// эта функция в зависимости от переданного флага рисует или стирает
// астероиды
for (index=0; index
{
// нарисовать астероид
if (erase==ERASE)
_setcolor(0);
else
_setcolor(asteroids[index].color);
// получаем позицию объекта
xo = asteroids[index].xo;
yo = asteroids [index].yo;
// перемещаемся в позицию первой вершины астероида
_moveto((int)(xo+asteroids[index]-vertices[О].p[X_COMP]), (int)(yo+asteroids[indexl.vertices[0].p[Y_COMP])) ;
for (vertex=l; vertex
{
_lineto((int)(xo+asteroids[index].vertices[vertex].p[X_COMP]), (int)(yo+asteroids[index].vertices[vertex].p[Y_COMP])) ;
} // конец цикла по вершинам
_lineto((int)(xo+asteroids[index].vertices[0],p[X_COMP]), (int)(yo+asteroids[index].vertices[0].p[Y_COMP]));
} // замкнуть контур объекта
} // конец функции
///////////////////////////////////////////////////////
void Translate_Asteroids(void)
{
// функция перемещает астероиды
int index;
for (index=0; index
{
// перемещать текущий астероид
asteroids[index].xo += asteroids[index].x velocity;
asteroids[index].yo += asteroids[index].y_velocity;
// проверка на выход за границы экрана
if (asteroids[index].xo > 600 || asteroids[index].xo < 40)
{
asteroids[index].x_velocity = -asteroids[index].x_velocity;
asteroids[index].xo += asteroids[index].x_velocity;
}
if (asteroids[index].yo > 440 1) asteroids[index].yo < 40)
{
asteroids[index].y_velocity = -asteroids[index].y_velocity;
asteroids[index].yo += asteroids[index].у_velocity;
)
} // конец цикла for
} // конец функции
////////////////////////////////////////////////////////
void Rotate__Asteroids()
{
int index;
for (index=0; index
{
// вращать
текущий астероид
Rotate_Object_Mat((object_ptr)&asteroids[index]);
} // конец цикла for
} // конец функции
///////////////////////////////////////////////////////
void main(void)
{
// перевести компьютер в графический режим
_setvideomode(_VRES16COLOR); // 640х480, 16 цветов
// инициализация
Create_Field();
while(!kbhit())
{
// стереть
поле
Draw_Asteroids(ERASE) ;
// преобразовать
поле
Rotate_Asteroids();
Translate_Asteroids() ;
// нарисовать
поле
Draw_Asteroids(DRAW);
// немного подождем...
Delay(500);
)
// перевести компьютер в текстовый режим
_setvideomode(_DEFAULTMODE);
} // конец функции
Вам потребуется время, чтобы изучить эту программу. Уделите внимание способу, которым астероиды масштабируются и поворачиваются. Если вы сравните время исполнения программ из Листингов 4.8 и 4.11, то не найдете никакой разницы. Если же вы потратите время и поместите все повороты, масштабирование и перемещение в одну матрицу, то программа из Листинга 4,11 будет работать значительно быстрее.
Итак, умножение матриц особенно эффективно, когда вы производите множественные трансформации объекта с помощью единой, заранее подготовленной матрицы, включающей в себя все операции преобразования над этим объектом.
Программируем системные часы
Внутренние часы компьютера, следящие за временем, на самом деле вовсе никакие не часы. Это микросхема типа таймер/счетчик, которая просто в, зависимости от начальных настроек с определенной частотой изменяет значение счетчика. Эта микросхема называется микросхемой таймера 8253. В ней содержится три шестнадцатиразрядных счетчика. Эти счетчики можно программировать самыми разнообразными способами. В таблице 12.2 показано назначение каждого из них.
Таблица 12.2. Счетчики микросхемы таймера 8253.
Порт ввода/вывода
|
|
Номер счетчика
|
|
Назначение
|
|
40h
|
|
Счетчик 0
|
|
Таймер/Диск
|
|
41h
|
|
Счетчик 1
|
|
Обновление памяти
|
|
42h
|
|
Счетчик 2
|
|
Накопитель на магнитной ленте
|
|
43h
|
|
Управляющий регистр
|
|
Управление таймером
|
|
Доступ к микросхеме таймера осуществляется через порты 40h-43h. Как видите, мы можем использовать только счетчики 0 и 3. Связываться со счетчиком 1 нам определенно не стоит! Счетчик 0 уже используется DOS для системных часов. Так почему бы нам не использовать его, перепрограммировав на нужную частоту?
Это вполне можно сделать. И этот метод не вызовет неполадок в компьютере. Единственный недостаток такого подхода состоит в том, что изменится системное время. Мы можем обойти это, сохранив время, за которое изменяется значение частоты таймера и по ходу игры отслеживать время с учетом этого изменения. Затем, перед окончанием работы программы, следует восстановить прежнее значение частоты и изменить системное время. Для сохранения и восстановления системного времени можно использовать функции Си.
Произведение операций над матрицами
Вы можете производить большинство операций над матрицами так же, как вы оперируете и с нормальными числами. Например, вы можете их складывать или вычитать, соответственно складывая или вычитая каждый из компонентов.
Для примера, рассмотрим сложение двух матриц размерностью 2х3 - матрицы А и матрицы С:
При сложении матриц А и С нужно складывать каждый из элементов m,n. Суммы элементов займут в результирующей матрице сответствующие места:
Мы также можем умножить матрицу на скаляр k. Например, чтобы умножить матрицу А на 3, мы должны умножить на 3 каждый ее элемент:
Теперь поговорим об умножении двух матриц. Эта операция немного отличается от умножения на скалярную величину. Вы должны запомнить несколько правил:
§
Количество столбцов в первой матрице (n) должно быть равно количеству строк во второй (также n). Это значит, что если размерность первой матрицы (mxn), то размерность второй матрицы должна быть (nхr). Два остальных измерения m и r могут быть любыми;
§ Произведение матриц не коммутативно, то есть А х В не равно В х А.
Умножение матрицы mxn на матрицу nхr может быть описано алгоритмически следующим образом:
1. Для каждой строки первой матрицы:
§ Умножить строку на столбец другой матрицы поэлементно.
§ Сложить полученный результат;
2. Поместить результат в позицию [i,j] результирующей матрицы, где i - это строка первой матрицы, a j - столбец второй матрицы.
Для простоты посмотрим на рисунок 4.9:
Мы можем это сделать намного проще, написав программу на Си. Давайте определим матрицу 3х3 и напишем функцию, умножающую матрицы. В Листинге 4.9 показана соответствующая программа.
Листинг 4.9. Определение и умножение двух матриц.
// общая структура матрицы
typedef sruct matrix_typ
{
float elem[3][3]; // место для хранения матрицы
} matrix, *matrix_ptr;
void Mat_Mult3X3 (matrix_ptr matrix_1, matrix_ptr matrix_2, matrix_ptr result)
{
index i,j,k;
for(i=0; i<3; i++)
{
for (j=0; j<3; j++)
{
result[i][j] = 0; // Инициализация элемента
for(k=0; k<3; k++)
{
result->elem[i][j]+=matrix_1->elem[i][k] * matrix_2->elem[k][j];
} // конец цикла по k
} // конец цикла по j
} // конец цикла по i
} // конец функции
Перед выходом из этой функции мы имеем результат, сохраненный в переменной result.
Производство кинофильма
Новой тенденцией в производстве видеоигр является использование оцифрованных образов реальных актеров и декораций. То, что я чувствую, глядя на такие игры, можно выразить так: «Ни за что!» Это же видеоигра! В ней только предполагается, что персонаж выглядит так же, как и в кино! Вообще- то оцифрованные декорации не так уж и плохи, если программа делает похожими на обычные "компьютерные" картинки и они не конфликтуют с игровыми объектами. Интересным техническим приемом, который может даже наихудшему из артистов придать великолепную внешность, является использование моделей игровых созданий совместно с камерой и устройством ввода и регистрации кадров изображения (фреймграббером). Рисунок 15.1 показывает, как можно было бы использовать реальные модели и видеоаппаратуру для этих целей.
В восьмой главе, «Высокоскоростные трехмерные спрайты», мы углубились в детали построения небольшой студии и использования видео для этого рода работы. То же самое вкратце:
§
Модель некоего создания помещена на платформу с голубым, черным или белым фоном, который может быть удален позднее;
§ Далее вы оцифровываете изображение, используя ПК, видеокамеру и фреймграббер.
В частности, так создавались изображения DOOM. Модельер изготавливает модели созданий, потом помещает их на платформу и оцифровывает. Затем фиксирует их цвета и отдает программистам, чтобы те использовали их в игре. Следовательно, получение различных ракурсов трехмерных объектов является таким же легким делом, что и поворот или передвижение модели. Потому если у вас есть немного денег, и ваши художественные таланты не зачахли стоит попробовать пойти этим путем.
«Прозрачные» пиксели
«Прозрачными» будем называть такие пиксели, которые при выводе на экран пропускаются и не перекрывают имеющееся изображение. Один из методов получения такого результата заключается в проверке значения цвета каждого пикселя перед тем, как он будет нарисован. Если цвет пикселя совпадает с «прозрачным», мы пропускаем данный пиксель и переходим к следующему. Такое дополнение к алгоритму ложится тяжелым бременем на нашу борьбу за скорость работы программы в процессе выполнения, а тем более — при выводе на экран. Ведь теперь мы не можем воспользоваться функцией memcpy() для вывода целой строки пикселей на экран, а должны применить цикл for() для изображения каждой точки отдельно.
Листинг 17.3 содержит новую функцию, называемую TransparentBlt(). Она заменит нам OpaqueBIt(). Разница между ними состоит только в том, что TransparentBlt() пропускает «прозрачные» пиксели (и это тоже тормозит работу программы).
Но как же TransparentBlt() отличает «прозрачные» пиксели от «непрозрачных»? Я решил, что любой пиксель со значением цвета, равным 0 (обычно, это черный) будет «прозрачным», но вы можете назначить для этого другой цвет. Функция пропускает любой пиксель, у которого значение цвета равно объявленной константе TRANSPARENT. Программа из Листинга 17.3 (PARAL1.C) является демонстрацией смещения двух повторяющихся слоев. Дальний слой сплошной, в то время как ближний включает в себя «прозрачные» пиксели. Для вывода изображений используются функции OpaqueBIt() и TransparentBit() соответственно. Несмотря на то, что у нас имеется всего два движущихся слоя, эффект получается довольно реалистичным. Как и в программе из Листинга 17.2, курсорные клавиши «влево» и «вправо» перемещают изображение по горизонтали, а для завершения программы нужно нажать Esc.
Обратите внимание, что скорость смены кадров в этой программе значительно ниже, чем в предыдущей. Это происходит из-за использования функции для работы с «прозрачными» пикселями. На компьютере с процессором 386SX/25 я получил примерно 10 кадров в секунду.
В принципе, это не так уж и плохо для программы, написанной полностью на Си.
Листинг 17.3. Простой двойной параллакс (PARAL1.C).
#include
#include
#include
#include
#include
#include "paral.h"
char *MemBuf, // указатель на дублирующий буфер
*BackGroundBmp, // указатель на битовую карту фона
*ForeGroundBmp, // указатель на битовую карту
// ближнего плана
*VideoRam; // указатель на видеобуфер
PcxFile pcx; // структура данных
// для чтения PCX-файла
int volatile KeyScan; // заполняется обработчиком
// прерывания клавиатуры
int frames=0, // количество нарисованных кадров
PrevMode; // исходный видеорежим
int background, // позиция прокрутки фона
foreground, //позиция прокрутки битовой карты
// ближнего плана position; // общее расстояние прокрутки
void _interrupt (*OldInt9)(void); // указатель на обработчик
// прерывания клавиатуры BIOS
// Функция загружает 256 - цветный PCX-файл
int ReadPcxFile(char *filename,PcxFile *pcx)
{
long i;
int mode=NORMAL,nbytes;
char abyte,*p;
FILE *f;
f=fopen(filename,"rb");
if(f==NULL)
return FCX_NOFILE;
fread(&pcx->hdr,sizeof(PcxHeader),1, f);
pcx_width=1+pcx->hdr.xmax-pcx->hdr.xmin;
pcx->height=1+pcx->hdr.ymax-pcx->hdr.ymin;
pcx->imagebytes=(unsigned int) (pcx->width*pcx->height);
if(pcx->imagebytes > PCX_MAX_SIZE) return PCX_TOOBIG;
pcx->bitmap= (char*)malloc (pcx->imagebytes);
if(pcx->bitmap == NULL) return PCX_NOMEM;
p=pcx->bitmap;
for(i=0;iimagebytes;i++)
{
if(mode == NORMAL)
{
abyte=fgetc(f);
if((unsigned char)abyte > 0xbf)
{ nbytes=abyte & 0x3f;
abyte=fgetc(f);
if(--nbytes > 0)
mode=RLE;
}
}
else if(-—nbytes == 0) mode=NORMAL;
*p++=abyte;
}
fseek(f,-768L,SEEK_END); // получить палитру,из PCX-файла
fread(pcx->pal,768,1,f);
p=pcx->pal;
for(i=0;i<768;i++) // битовый сдвиг цветов в палитре
*р++=*р >>2;
fclose(f) ;
return PCX_OK;
}
// Новый обработчик прерывания клавиатуры для программы прокрутки
// Он используется для интерактивной прокрутки изображения.
// если стандартный обработчик прерывания 9h не будет заблокирован
// длительное нажатие на клавиши управления курсором приведет
// к переполнению буфера клавиатуры и появлению крайне неприятного
// звука из динамика.
void _interrupt Newlnt9(void)
{
register char x;
KeyScan=inp(0х60);// прочитать код клавиши
x=inp(0x61); // сообщить клавиатуре, что символ обработан
outp(0x61, (х|0х80));
outp(0х61,х);
outp(0х20,0х20); // сообщить о завершении прерывания
if(KeyScan == RIGHT_ARROW_REL ||// проверка кода клавиши
KeyScan == LEFT_ARROW_REL) KeyScan=0;
}
// Функция восстанавливает исходный обработчик прерываний клавиатуры
void RestoreKeyboard(void)
{
_dos_setvect(KEYBOARD,OldInt9); // восстанавливаем
// обработчик BIOS
}
// Эта функция сохраняет прежнее значение вектора прерывания // клавиатуры и устанавливает новый обработчик нашей программы.
void InitKeyboard(void)
{
OldInt9= _dos_getvect(KEYBOARD); // сохраняем адрес
// обработчика BIOS
_dos_setvect(KEYBOARD,NewInt9); // устанавливаем новый
// обработчик прерывания 9h
}
// Эта функция использует функции BIOS для установки в регистрах
// видеоконтроллера значений, необходимых для работы с цветами,
// определяемыми массивом раl[]
void SetAllRgbPalette(char *pal)
{
struct SREGS s;
union REGS r;
segread(&s); // читаем текущее значение сегментных регистров
s.es=FP_SEG((void far*)pal); // в ES загружаем сегмент ра1[]
r.x.dx=FP OFF((void far*}pal);// в DX загружаем смещение pal[]
r.x.ax=0xl012; // готовимся к.вызову подфункции // 12h функции BIOS 10h
r.x.bx=0; /;/ номер начального регистра палитры
r.х.сх=256; // номер последнего изменяемого регистра
int86x(0xl0,&r,&r,&s);// вызов видео BIOS
}
// Функция устанавливает режим 13h
// Это MCGA-совместимыЙ режим 320х200х256 цветов
void InitVideo()
{
union REGS r;
r.h.ah=0x0f; // функция Ofh - установка видеорежима
int86(0xl0,&r,&r); // вызов видео BIOS
PrevMode=r.h.al; // сохраняем старое значение режима
r.x.ax=0xl3; // устанавливаем режим 13h
int86(0х10,&r,sr); // вызов видео BIOS
VideoRam=MK_FP(0xa000,0); // создаем указатель на видеопамять
}
//Эта функция восстанавливает исходный видеорежим
void RestoreVideo()
{
union REGS r;
r.x,ax=PrevMode; //исходный видеорежим
int86(0х10,&r,&r); // вызов видео BIOS
}
// Функция загрузки битовых карт слоев
int InitBitmaps()
{
int r;
// начальное положение линии деления
background=foreground=1;
// читаем битовую карту фона
r=ReadPcxFile("backgrnd.pcx",&pcx);
// проверка на ошибки чтения if(r != РСХ_ОК)
return FALSE;
// запоминаем указатель на битовую карту
BackGroundBmp=pcx.bitmap;
// устанавливаем палитру
SetAllRgbPalette(pcx.pal) ;
// читаем битовую карту переднего слоя
r=ReadPcxFile("foregrnd.pcx",&pcx);
// проверка на ошибки чтения
if (r != РСХ_ОК) return FALSE;
//запоминаем указатель на битовую карту
ForeGroundBmp=pcx.bitmap;
// создаем буфер в памяти
MemBuf=malloc(MEMBLK);
// проверка на ошибки распределения памяти
if(MemBuf == NULL) return FALSE;
memset(MemBuf,0,MEMBLK); // очистка буфера
return TRUE;
// все в порядке!
}
// функция освобождает выделенную память
void FreeMem()
(
free(MemBuf);
free(BackGroundBmp);
free(ForeGroundBmp);
}
// Функция рисует слои параллакса.
// Порядок отрисовки определяется координатой слоя по оси Z.
void DrawLayers()
{
OpaqueBlt(BackGroundBmp,0,100,background);
TransparentBlt(ForeGroundBmp,50,100,foreground);
}
// Эта функция осуществляет анимацию. Учтите, что это наиболее
// критичная по времени часть программы. Для оптимизации отрисовки
// как сама функция, так и те функции, которые она вызывает,
// следует переписать на ассемблере.
Как правило, это увеличивает
// быстродействие на 100 процентов.
void AnimLoop()
{
while(KeyScan != ESC_PRESSED) // пока не нажата клавиша ESC
(
switch(KeyScan) // определяем, какая клавиша была нажата
{
case RIGHT_ARROW_PRESSED: //нажата "стрелка вправо"
position--; // изменяем позицию
if(position < 0) // останавливаем прокрутку,
// если дошли до конца
{
position=0;
break;
} backgrpund —=1; // прокручиваем фон влево на 2 пикселя
if(background < 1) // дошли до конца?
background+=VIEW_WIDTH; // ...если да - возврат к началу
foreground-=2; // прокручиваем верхний
// слой влево на 4 пикселя
if(foreground < 1) // дошли до конца?
foreground+=VIEW_WIDTH; // ...если да - возврат к началу
break;
case LEFT_ARROW_PRESSED: // нажата "стрелка влево"
position++; // изменяем текущую позицию прокрутки
if(position > TOTAL_SCROLL) // останавливаем прокрутку,
// если дошли до конца
{
position=TOTAL_SCROLL;
break;
}
background+=l; // прокручиваем фон вправо на 2 пикселя
if(background > VIEW_WIDTH-1) // дошли до конца?
background-=VIEW_WIDTH; // ...если да - возврат к началу
foreground+=2; // прокручиваем верхний слой
// вправо на 4 пикселя
if(foreground > VIEW_WIDTH-1) // дошли до конца?
foreground-=VIEW_WIDTH; // ...если да - возврат к началу
break;
default: // игнорируем остальные клавиши
break;
}
DrawLayers(); // рисуем слои в буфере в
// оперативной памяти
memcpy(VideoRam,MemBuf,MEMBLK); // копируем буфер в
// видеопамять
frames++; // увеличиваем счетчик кадров
) }
//эта функция осуществляет необходимую инициализацию
void Initialize()
{
position=0;
InitVideo(); // устанавливаем видеорежим 13h
InitKeyboard(); // устанавливаем наш обработчик
// прерывания клавиатуры
if(!InitBitmaps()) // загружаем битовые карты
{
CleanUp(); //освобождаем память
printf("\nError loading bitmaps\n");
exit(1);
} }
// функция выполняет всю необходимую очистку
void Cleanup()
{
RestoreVideo(); // восстанавливаем исходный видеорежим
RestoreKeyboard(); // восстанавливаем обработчик
// прерывания клавиатуры BIOS
FreeMem(); // освобождаем всю выделенную память
}
// Это начало программы. Функция вызывает процедуры инициализации.
// Затем читает текущее значение системного таймера и запускает
// анимацию. Потом вновь читается значение системного таймера.
// Разница между исходным и конечным значениями таймера
// используется для вычисления скорости анимации.
int main()
{
clock_t begin,fini;
Initialize(}; // проводим инициализацию
begin=clock(); // получаем исходное значение таймера
AnimLoop(); // выполняем анимацию
fini=clock(); // получаем значение таймера
CleanUp(); // восстанавливаем измененные параметры
printf("Frames: %d\nfps: %f\n",frames,
(float)CLK_TCK*frames/(fini-begin));
return 0;
}
Рассеянное освещение
Рассеянное освещение создает находящийся в комнате источник света. Например, если у вас есть регулируемая лампа, то вы можете изменять уровень рассеянного освещения. Рисунок 6.32 показывает комнату при двух разных уровнях рассеянного освещения.
Естественно, чем большим количеством-источников света вы располагаете, тем больше у вас возможностей воздействовать на рассеянную освещенность. Самый хороший пример общей освещенности - солнечный свет. Солнце находится от нас на столь большом, расстоянии, что падающие на землю лучи можно считать параллельными. Это создает некоторую среднюю или рассеянную освещенность, интенсивность которой зависит от высоты солнца над горизонтом.
Раздел 1: Инициализация
В этой части программы мы загружаем все файлы со звуковыми эффектами И графикой для игры. Элементы изображения для танков берутся из загруженный файлов и размещаются в предварительно выделенной под буфер области памяти. Net-Tank использует технику дублирующей буферизации для исключения мерцания изображения. Напомню, это означает, что изображение вначале формируется в оперативной памяти и в уже полностью подготовленном виде копируется в видеопамять. Кроме того, во время инициализации структуры данных, описывающие все игровые объекты, обнуляются или устанавливаются в исходное положение.
Уже при инициализации возникает первый вопрос сетевой игры. Программы работающие на обоих машинах почти идентичны, но должна быть небольшая разница: ваш компьютер должен показать объект противника в том же месте, где он изначально расположен на другом ПК и наоборот. Это означает, что иные позиции игровых объектов должны быть жестко установлены. Этого можно добиться, задав позиции в качестве констант в тексте программы, загружая их из неизменяемого файла данных или задавая при старте программы определенному алгоритму. Способ, с помощью которого это делается, не важен. Однако при старте программы игрок, который с точки зрения одной машины является местным, будет удаленным с точки зрения другого компыотера наоборот. Это может привести к путанице с координатами и проблемам с синхронизацией.
Раздел 2: Игровой цикл
Следующая часть начинается с главного цикла. Обратите внимание, что в игре имеется два цикла: внешний и внутренний. Внешний игровой цикл используется для инициализации некоторых переменных. Затем начинается внутренний цикл. Именно в нем и происходит основное действие.
Заметьте, что игра различает, когда она находится в состоянии соединения, а когда - нет.
Раздел 3: Удаление объектов
Как вы узнали из предыдущих глав, посвященных графике, прежде чем рисовать спрайты на новом месте, мы должны удалить их из прежней позиции. Эта часть программы убирает с игрового поля все движущиеся объекты путем восстановления ранее сохраненного фона под ними.
Раздел 4: Получение входных данных и передача состояния дистанционно управляемой системе
Здесь начинается самое приятное. Эта часть программы подразделена на два фрагмента:
§
Первый из них принимает входные данные от локального игрока;
§ Другой принимает входные данные от удаленного игрока.
Любопытно то, что оба фрагмента делают практически одно и то же. Разница только в том, что второй фрагмент обращает больше внимания на то, что поступает из последовательного порта, а первый в основном интересуется клавиатурой. Давайте остановимся и чуть-чуть поговорим о том, как происходит соединение. Как я говорил несколькими страницами раньше, для осуществления соединения применяются два основных метода:
§ Можно передавать состояние игры в целом;
§ Вы можете посылать статус устройств ввода и трактовать это как прием данных от другого джойстика или клавиатуры.
В Net-Tank я применил второй метод. Один раз в течение цикла второй машине передаются все манипуляции игрока с клавиатурой. В это же время другой компьютер интерпретирует полученные по сети данные как действия со своей собственной клавиатурой.
Помните, чтобы этот технический прием работал, обе игры должны быть полностью детерминированы. Никаких случайностей быть не должно. В Net-Tank я полностью следовал этому правилу всюду, кроме фрагмента, изображающего взрыв. Обычно этого не сложно избежать, но иногда, после уничтожения одного из танков, игры теряют синхронизацию.
Раздел 5: Перемещение объектов
Следующая часть программы занимается перемещением объектов. Мы просто используем информацию, поступающую от локального и дистанционного ввода для передвижения и разворота танков. Интересен способ перемещения танков. Они могут двигаться в 16 различных направлениях, угол между которыми составляет 22.5 градуса. Вообще-то, чтобы переместить танк в выбранном направлении нам потребовалось бы, прежде всего, найти угол, а затем вычислить его синус и косинус для нахождения параметров передвижения.
Однако в нашей игре синусы и косинусы вычислены заранее. Это позволяет присвоить каждому из направлений свой номер (который будет совпадать с номером текущего кадра, изображающего танк) и использовать его как индекс в таблице, содержащей значения соответствующих передвижений по осям координат.
Поверьте, это лучще, чем использование математики с плавающей запятой, да еще вместе с тригонометрическими функциями. Вы узнаете гораздо больше о подобных фокусах в восемнадцатой главе, "Техника оптимизации".
Во всяком случае, как только танки двинутся, сразу же можно открывать огонь.
Раздел 6: Распознавание столкновений
Когда для всех объектов определено их новое местоположение, нужно посмотреть, не пересекаются ли они в каких-нибудь точках, то есть не нарушили ли они принцип исключительности Паули, полагающий, что две различные частицы, не могут в одно и то же время находиться в одном и том же месте. Применительно к нашей программе, мы должны ответить на вопрос: попал ли снаряд в вражеский или в наш собственный танк? Для этого координаты всех снаряда проверяются на совпадение с координатами танка. Если происходит попадание оно фиксируется с тем, чтобы в конце цикла игры изобразить взрыв.
Что касается столкновений танков друг с другом, то пока позволим им это делать беспрепятственно (предлагаю вам доработать программу в этой части самостоятельно). Кроме этого нам нужно позаботиться о том, чтобы танки не могли проходить сквозь стены. Если вы помните, игровое поле представляет собой матрицу элементов, имеющую определенную размерность. В Net-Tank размер игрового поля составляет 20х11 ячеек, каждая из которых имеет площадь 16х16 пикселей. Следовательно, чтобы увидеть, не столкнулся ли танк со стенкой, то есть попал в занятую ячейку, необходимо:
§
Разделить обе координаты танка на 16;
§ Округлить результат до целого;
§ Использовать полученное значение как индекс ячейки игрового поля, чтобы увидеть, есть ли там блок. Если столкновение произошло, вернуть танк в прежнюю позицию.
Раздел 7: Рисование объектов
Теперь мы готовы нарисовать все объекты. Чтобы сделать это, мы должны сохранить фон в тех местах, где мы планируем разместить объекты. После этого их можно нарисовать. На этом этапе игровой экран полностью построен в дублирующем бу4эере и теперь можно посмотреть, что же у нас получилось.
Раздел 8: Дублирующий буфер
В Net-Tank используется техника дублирующей буферизации для исключения мерцания изображения. В этой части игры крайне быстрый цикл ассемблера копирует дублирующий буфер на экран. Однако мы перемещаем только первые 176 строк из буфера, .так как нижняя часть экрана неизменна и ее не надо перерисовывать.
Раздел 9: Всякая всячина
Игра прошла очередной цикл. Новая игровая ситуация отображена на экране и мы готовы вернуться к началу цикла игры. Но перед этим нам нужно сделать кое-что еще. Во время этой фазы игры перерисовывается игровое пространство, сверкают огни взрывов, а переменные, нуждающиеся в сбросе, принимают исходные значения.
Разработка 256-цветной палитры
Существует несколько различных подходов к разработке палитр для 256-цветных игр. Один метод, обычно используемый в играх приключенческого типа заключается в том, чтобы определить часть палитры, неизменной для всех изображений. Эти цвета используются для изображений, появляющихся более чем на одном игровом экране (например, это могут быть элементы пользовательского интерфейса, персонажи и некоторые другие объекты). Обычно для этого достаточно 64 цветов. Оставшиеся 192 цвета палитры будут изменяться от экрана к экрану в зависимости от декораций, сопровождающих игру. Другой подход состоит в создании единой палитры, используемой для всех пейзажей, объектов и персонажей игры. Этот метод мы здесь и обсудим.
Наилучшие для использования цвета
Если вы разрабатываете все изображения своей игры с нуля, у вас есть обширный выбор цветов для создания собственной игровой палитры. Однако, как правило, вы будете применять следующие диапазоны цветов:
§ Оттенки серого (от чисто-белого до чисто-черного);
§ Все оттенки основных цветов (красного, желтого, синего);
§ Оттенки каждого из вторичных цветов (оранжевого, зеленого, фиолетового);
§ Телесные тона для персонажей;
§ Земляные краски для земли, леса и т. д.
Последовательный порядок цветов
Что я понимаю под словами «диапазон цветов» или «последовательный порядок цветов»? Рассмотрите файл ЕХАМР01.РСХ. К примеру, вы хотите изобразить красный шар так, чтобы он выглядел действительно круглым, объемным. Он должен располагаться на черном фоне и освещаться с правой фронтальной стороны. На плоскости этот шар был бы представлен кругом, заполненным однородным цветом, средним оттенком красного. Такое представление шара показано в верхней части на рисунке 16.1.
Едва ли это убеждает, не так ли? Чтобы шар выглядел объемным, оттенки красного должны плавно переходить от светлого к темному, что придаст объекту соответствующую форму и подсветку.
Тот же самый технический прием применяется к прямоугольным и к цилиндрическим формам, как показано на рисунке.
Вот такой плавный переход оттенков одного цвета от светлого к темному и называю диапазоном или последовательным порядком цветов. Рассматривая файл ЕХАМР01.РСХ, обратите внимание, что палитра для него имеет несколько различных цветовых диапазонов. Попробуйте поэкспериментировать с это палитрой и изменить ее в соответствии с вашим цветовым предпочтением.
Сохранение цвета для прозрачности
При разработке палитры ни в коем случае нельзя забывать о том, что вы должны зарезервировать «прозрачный» цвет для вывода спрайтов. Если увидите пустоту в каком-то изображении, то, скорее всего, вы допустили ошибку. Помните, что «прозрачный» цвет при наложении спрайтов не изменит цвет фона.
Применение палитр других изображений
Существует немало коммерческих библиотек готовых изображений и фотографии, которые можно использовать без выплаты дополнительного вознаграждения их создателям. Однако, чтобы быть уверенным в том, что у вас действительно есть право на использование этих изображений в собственных программах, необходимо прежде всего очень внимательно прочитать липензионное соглашение для таких коллекций. Если у вас возникнут на этот счет какие-либо сомнения, всегда входите в контакт с издателем библиотеки.
Ни в коем случае не рекомендуется использование изображений, взятых из ББС или других коммуникационных служб. Многие находящиеся там графические файлы были просто скопированы из журналов, передач и фильмов без соблюдения соответствующих авторских прав. Постарайтесь ничего не испольвать в вашей игре без разрешения, полученного из сопроводительной документации. Безопаснее всего, конечно, употреблять только собственноручно изготовленные изображения и фотографии, либо, если нет возможности заниматься этим самостоятельно, нанять кого-нибудь специально, для разработки графики.
В предыдущем разделе мы обсуждали, какие цвета лучше всего размещать в палитре, чтобы иметь наиболее широкие возможности использования цветового диапазона.
Если вы попытаетесь какую- либо фотографию подогнать под имеющуюся палитру, то очень скоро убедитесь, что ее внешний вид окажется несколько изменен. Это может стать величайшим образцом поп-арта, но вряд ли будет пригодно для вашей игры. Чтобы обойти такое затруднение, необходимо создать палитру, включающую цвета, которые присутствуют в изображении.
Если вы намерены использовать в вашей игре фотографии и другие оцифрованные изображения, набора в 256 цветов хватит ненадолго. Запомните несколько правил:
§ Подбирайте изображения, имеющие сходную цветовую палитру, причем стремитесь, чтобы она была по возможности ближе к той, которую вы хотите использовать в своей игре. Это правило позволит вашим картинкам отображаться более точно;
§ Выбирайте изображения достаточно контрастные и в то же время с непротиворечивым цветовым решением. Если вы возьмете изображение, светлое с одного края и темное — с другого, это создаст лишние сложности в использовании принципа мозаичности, необходимого для трехмерных игр. Мы обсудим способы получения мозаичных изображений чуть позже;
§ Не берите изображения с большим количеством мелких деталей. К примеру, у вас есть фотография, выполненная с высоким разрешением, изображающая компанию людей. Когда это изображение уменьшится до размеров 64х64 пикселя, оно, скорее всего, будет сильно отличаться от оригинала. Лица людей на нем могут оказаться представленными всего одним пикселем, а то и вовсе пропадут!
Вероятно, хорошей идеей будет отмасштабировать ваши фотографии или другие оцифрованные изображения до окончательного размера, в котором они предстанут в игре. Изменять масштаб лучше всего перед тем, как вы начнете оптимизировать палитру. На то есть две причины:
§ Во-первых, после изменения масштаба пропадет часть цветов, что облегчит оптимизацию палитры;
§ Во-вторых, когда придет время собирать разрозненные изображения единый PCX-файл с общей палитрой, этот процесс пойдет намного легче.
В зависимости от того, используете ли вы 256-цветный или 24-битный графический редактор, нужно по-разному подходить и к созданию окончательной 256-цветной палитры.
Как можно скомбинировать палитры нескольких картинок без ухудшения изображения при использовании 256-цветной программы рисования? Один подход состоит в том, чтобы уменьшить количество цветов, насколько это возможно без ущерба для качества.
Позвольте мне продемонстрировать это на примере. Взгляните на рисунок 16.2. Слева изображена комната, как она выглядит при дневном освещении, а справа показан тот же самый вид ночью. Рисунок 16.3 ничем не отличается от предыдущего за исключением того, что палитра перекомпонована по градиентам, Сохранение цветов, уже имеющихся в изображении, упростит создание трехмерного изображения от руки.
Теперь посмотрим на рисунок 16.4 (файл ЕХАМР04.РСХ на дискете). В этом файле количество цветов, содержащихся в изображении, может быть уменьшено до 128. Это приведет к небольшому ухудшению качества изображения, но не сильно испортит общий вид рисунка. Зато у вас высвободится половина цветов, которые могут быть использованы для других картинок.
Посмотрим, что произойдет, если мы уменьшим количество цветов еще наполовину, то есть до 64. Взгляните на рисунок 16.5 (файл ЕХАМР05.РСХ). Качество изображения снова ухудшилось и теперь даже может потребоваться слегка подкрасить его вручную. Сокращение палитры можно продолжать и дальше, и только вы решаете, остается ли качество все еще достаточно приемлемым для вашей игры.
Подобным образом нужно обработать все изображения, которые вы хотите ввести в игру. Но не забывайте, что общее количество цветов после объединения всех рисунков не может превышать 256.
Следующим шагом было бы неплохо собрать все изображения, палитр которых вы сократили, в одном графическом файле.
Некоторые программ позволяют объединять палитры нескольких изображений в одной, и вы можете использовать ее для разработки оставшихся картинок вручную.
В 24-битовом графическом редакторе несколько проще создать новое изображение, включающее в себя все или почти все собранные вами промасштабированпые картинки. Например, на экране с разрешением 640х480 точек без труда можно разместить от 50 до 60 изображений размером 64х64 пикселя, даже если оставлять между ними для большей наглядности некоторый зазор. Кроме того, работая с 24-битовой программой, вам не нужно беспокоиться насчет сжатия палитры каждого изображения до нескольких цветов. Вы можете поместить все картинки в один файл, а затем просто отконвертировать его в 256-цветное изображение. Программа сама отберет для палитры те цвета, которые наиболее часто встречаются в изображениях.
Специальные соображения по поводу цифрового видео
Цифровые видеокадры значительно чаще используются для представления персонажей, чем образы, нарисованные от руки. С приходом Microsoft Video for Windows объединение оцифрованных видеофрагментов в играх и другом программном обеспечении является вполне обычным делом. Если ваша игра должна включать цифровые видеопоследовательности, убедитесь, что в каждый кадр вставляется одна и та же палитра. Для выполнения этой задачи у вас должны иметься соответствующие инструментальные средства. Усечение цветов в цифровой видеопоследовательности — нелегкое дело, особенно если отснято достаточно большое количество кадров. Обычно неплохо транслируются последовательности типа неподвижной «говорящей головы», потому что цвета в каждом кадре довольно хорошо согласуются между собой. Также можно снимать персонаж, расположив его перед синим экраном и осветив его так, чтобы на фон не падало никаких теней. Затем образ персонажа может быть наложен на любой другой фон с помощью специального технического приема, называемого Chromakeing (хромакей). Такой прием обычно используется в телевизионных передачах, например, для совмещения изображения ведущего с картами погоды (более подробно об этой процедуре говорилось в восьмой главе, «Высокоскоростные трехмерные спрайты»),
РАЗРАБОТКА МУЗЫКИ ДЛЯ КОМПЬЮТЕРНЫХ ИГР
Теперь вы знаете о методах оптимизации и других графических трюках и хитростях. Вы уже научились тому, как сделать графику ослепительной, как наделить игру искусственным разумом и, может быть, даже умеете считывать данные из портаджойстика. В этой главе мы расскажем о самом мощном методе длительного эмоционального воздействия на игрока. Звук и музыка позволят вам непосредственно управлять эмоциями пользователя в соответствии с контекстом вашей игры. Хорошо разработанные звуковые эффекты могут создать даже более яркую «виртуальную реальность», чем самая изощренная трехмерная графика. Эмоциональное воздействие музыки очень разнообразно, она может передавать ощущение страха, волнения, победы, грусти и даже ужаса.
Музыкальные и звуковые эффекты - это наиболее мощный инструмент, способный воздействовать на игрока на уровне эмоций. Если для вас это не очевидно, посмотрите снова фильм Спилберга «Парк юрского периода», В тот момент, когда тиранозавр появляется, чтобы съесть Ленд Ровера, выключите звук и тогда вы поймете, что весь ужас ситуации полностью создается за счет музыки и звука. Если графика позволяет игроку увидеть вашу «виртуальную реальность», то музыка и звуковые эффекты дают ему возможность почувствовать ее.
Хотя для многих из нас все это интуитивно понятно, вы должны практически проверить это на себе. Возьмите, например, «Терминатор II», «Конан-Варвар», «Звездные войны», «Чужие» или «Парк юрского периода». Каждый раз, когда по ходу фильма ваше сердце учащенно забьется, закройте глаза и попробуйте проанализировать только звуковое сопровождение, которое вы сейчас слышите. Послушайте, как тесно переплетается основная мелодия музыкальной партитуры фильма с его отдельными эпизодами, позволяя им зарождаться и развиваться. Как-нибудь, во время действительно шумной сцены, выключите звук. Вы почувствуете, что напряженность ситуации исчезла, словно в темной комнате вдруг зажгли свет.
Нет ничего из того, что создается в фильмах звуком и музыкой, что нельзя было бы повторить с помощью мультимедиа.
За одним только исключением. Наши звуковые эффекты и музыка должны быть интерактивны и должны совпадать с контекстом игры. Добавление интерактивных элементов в звуковое сопровождение игры многократно увеличивает ее эмоциональную напряженность.
Звуковое сопровождение содержит четыре основных компонента: диалоги, звуковые эффекты, фон и музыку. Ниже мы коротко рассмотрим каждый из этих компонентов и их роль в компьютерной игре.
Диалоги. Большинство диалогов, которые вы слышите в кино, переделываются в студии уже после того, как сцена была отснята. Эта процедура позволяет актеру (или актрисе) сфокусироваться на том, как звучат его (или ее) слова и, кроме того, позволяет звукорежиссеру контролировать точный аудиобаланс в окончательной звуковой дорожке. Все ваши диалоги должны быть сделаны профессионально и, желательно, в студии звукозаписи, Однако если у вас нет продюсера с громадным капиталом, вы, вероятно, не сможете ангажировать актеров Голливуда и снять классную студию звукозаписи. Однако вы можете привлечь к своей работе одного из специалистов в области мультимедиа. Обычно такие специалисты предлагают полный спектр услуг по озвучиванию компьютерных программ, включая наем актеров, звуковые эффекты на заказ и микширование звуковой дорожки. Кроме того, они могут представить звуковые данные в том формате, который требуется для вашего программного и аппаратного обеспечения.
Звуковые эффекты. Несмотря на доступность все большего количества звуковых библиотек, во многих случаях все же требуется создание своих оригинальных звуковых эффектов. Естественно, если что-то из библиотек подходит для вашей игры, вы можете это использовать, но в любом случае стоит проконсультироваться с опытным,в области мультимедиа специалистом, чтобы создать для своего проекта ряд оригинальных звуковых эффектов.
Фон. Фон - это те шумовые эффекты, на которые вы при просмотре фильма не обращаете внимания, однако их отсутствие ощущается мгновенно. Фон включает в себя звуки шагов, шумы автомобилей, ветра, щебетание птиц и другие звуки, окружающие нас в повседневной жизни.
Само собой разумеется, что фон должен быть связан с контекстом фильма. В кино каждый шаг и фабричный гудок добавляется в звуковую дорожку и покадрово синхронизируется уже после того, как фильм снят.
Шумовые эффекты создают большее ощущение «виртуальной реальности», чем точнейшая компьютерная графика. Воздействие шумовых эффектов многократно усиливается, если они проходят специальную цифровую обработку типа Qsound или реверберации. Реверберация — это метод обработки звукового сигнала, в результате которого на исходный звук накладывается эхо и отражение от поверхностей, так же, как это происходит в реальной обстановке. Например, для звуковой карты Creative Labs AWE 32 можно запрограммировать точные геометрические характеристики комнаты для каждого момента времени. Это делается с помощью механизма событий MIDI, при этом все шумовые эффекты звучат так, как если бы они происходили в настоящей комнате определенной формы и размеров.
Фон и цифровые эффекты - самая интерактивная составляющая звуковой дорожки. Благодаря шумовым эффектам игрок слышит звук шагов, грохот выстрелов, рычание приближающихся монстров, завывание ветра, щебет птиц, шум улицы. Воспроизведение таких звуков в реальном времени и в полном соответствии с вашей «виртуальной реальностью», позволяет игроку глубоко погрузиться в созданный вами мир. Удачное фоновое оформление усиливает ощущение «виртуальной реальности» больше, чем всевозможные очки, перчатки, шлемы и другие приспособления для компьютерных игр. Пренебрегая возможностями звуковых эффектов, вы отбрасываете пользователя во времена немого кино.
Музыка. Уже много было сказано по поводу того, как адаптировать музыку к интерактивной природе продуктов мультимедиа. В фильме события происходят последовательно. Поэтому композитор точно знает, с какого момента необходимо начинать нагнетать страсти. В компьютерных играх этот момент неизвестен; он зависит от того, когда же, наконец, игрок откроет ту самую «дверь, ведущую в Ад». Поэтому в одних играх просто в разных эпизодах звучат разные мелодии, тесно не связанные с происходящим, в других же авторы пробуют экспериментировать с разветвлением MIDI-музыки, пытаясь создать плавные переходы от одного сюжета к другому.
Были даже попытки создавать алгоритмическую музыку, то есть музыку, синтезируемую компьютером в реальном времени.
Вероятно, наиболее оптимальным решением будет собрать вместе всевозможные эмоции, возникающие по ходу игры, и обратиться к профессиональному композитору. То есть поступить так, как если бы вы снимали фильм. Композитор создаст музыкальный фон для каждой из ситуаций, а вы сможете его использовать по контексту игры. Таким образом, с одной стороны, эти мелодии не будут написаны для четко определенных моментов игры, а с другой, они все же будут связаны с эмоциональным контекстом. Поэтому, когда во время игры пользователь окажется в нервозной ситуации или попадет в опасность, вы с помощью музыки сможете усилить эти ощущения.
Другой подход к музыкальному сопровождению заключается в использо вании музыки просто для передачи общего настроения текущего эпизода. Основной же упор в этом случае делается на фон, диалоги и звуковые эффекты. Несомненно, звуки выстрелов, взрывов или вопли ужаса -отличный способ доходчиво рассказать игроку о том, что же происходит в игре в данный момент.
Нам, как разработчикам игр, очень повезло, что мы имеем в своем распоряжении очень мощные инструменты для создания звукового оформления. А ведь еще пару лет назад вы не смогли бы добиться от компьютера ничего, кроме бибиканья, свиста и треска. Естественно, что такое звуковое сопровождение вызывало желание побыстрее отыскать клавишу «выключить звук». Возможности первого поколения звуковых карт не многим отличались от возможностей встроенного динамика персонального компьютера. Несмотря на то, что звуковые карты, типа Adiib Personal Music System позволяли добавить в игру Интерактивные звуковые эффекты, их эмоциональное воздействие было еще очень ограниченным. Основная слабость пресловутых частотных синтезаторов состояла в том, что эмоциональный накал их музыки мог соревноваться разве что с мелодией, исполненной на детской дудочке.
К счастью, положение вещей кардинально улучшилось с появлением дисков CD-ROM, цифровых звуковых карт и MIDI-синтезаторов с волновыми таблицами.
Теперь компьютерные звуки и музыка могут поспорить в эмоциональном наполнении даже с картинами Спилберга. Я не оговорился, действительно могут! Ведь когда мы смотрим картины Спилберга, мы не можем принимать непосредственного участия в событиях. Мы видим, как динозавр атакует Ленд Ровера, но мы не можем вмешаться в ситуацию. В компьютерной же игре мы можем попытаться спастись от динозавра. Причем, когда мы будем убегать от злобного зверя, музыка и звуковые эффекты тесно сплетутся с нашими действиями. В результате наши нервы будут напряжены до предела — и не стоит забывать, что такого единства действия и звука можно достичь только в интерактивном мире!
Игра DOOM фирмы Id Software - один из лучших примеров использования интерактивного цифрового звука; Кто из вас не сжимался от страха на своем стуле, слыша, как из какого-нибудь угла раздается жуткое рычание и храпение монстра. Обратите внимание на то, что вы так бурно реагируете, даже еще не видя, а только слыша чудовище. Эмоциональное напряжение так велико, что когда, наконец, монстр выскакивает из-за укрытия, а затем, зашатавшись, падает под градом ваших пуль, вы испытываете самое настоящее чувство огромного облегчения. Все это достигается в немалой степени именно за счет звуковых эффектов, создающих у игрока соответствующее настроение. Удачный эффект помогает пользователю еще глубже окунуться в мир созданной вами игры.
Но я должен вас предостеречь. Насколько правильное использование звуковых эффектов и музыки усиливает игру, настолько же их плохое качество портит ее. Неудачные и не соответствующие эмоциональному настрою игры эффекты и музыка - это потеря денег, времени и дискового пространства. Даже если плохой, непрофессионально сделанный звук и не погубит окончательно вашу игру, он, несомненно, произведет на игрока отрицательное впечатление независимо от качества всей остальной игры.
Для того чтобы избежать подобных ошибок и сделать музыку и звук в вашей игре максимально эффективными, поступайте следующим образом:
§
Используйте звуковые эффекты, созданные профессионалами. Для этого либо обращайтесь к специалисту по мультимедиа, либо очень тщательно отбирайте эффекты из звуковых библиотек. Не воруйте звуковые эффекты из фильмов, с пластинок и телевидения. Все они защищены авторскими правами. Распространение вашей игры в этом случае будет запрещено и вам даже возможно придется объясняться в суде. Если вы считали замечательные звуковые эффекты из «Звездных войн» с BBS — это еще не значит, что вы имеете право их использовать в игре. Если в библиотеках звуковых клипов вам не удается найти звуковые эффекты, полностью удовлетворяющие вашим требованиям, остается только обратиться к специалисту по мультимедиа;
§ Используйте музыку, написанную профессионалами. Для этого либо обратитесь к композитору, создающему компьютерную музыку, либо используйте подходящие к вашей игре высококачественные клипы. Учтите, что композитор должен работать именно в жанре компьютерной музыки, иначе у него возникнут такие же трудности, как у бас-гитариста, который вдруг решил играть на саксофоне. Поэтому то, что данный человек — великий музыкант, еще не значит, что он способен создать классную midi-композицию для вашей игры. Необходимо, чтобы композитор умел обходить все слабые стороны и эффективно использовать все преимущества MIDI;
§ Убедитесь, что в любой момент времени музыка поддерживает эмоциональное напряжение в игре и не расходится с контекстом. Помните об интерактивной природе вашей музыки и о том, что она должна быть связана с контекстом игры. Чем больше внимание вы уделите этому, тем сильнее будет эффект;
§ Посмотрите фильмы, похожие на вашу игру. Каждый раз, когда вы будете их смотреть, попробуйте осознать, каким образом ваши эмоции усиливаются звуковыми эффектами и музыкой. Если вы не обладаете хорошим музыкальным слухом или если вы четко не представляете, как эффективно использовать звук и музыку в игре, вам следует передать эту часть проекта кому-нибудь другому;
§ Прислушивайтесь к мнению своего композитора. Найдите композитора с опытом работы с вашим аппаратным обеспечением и хорошо ориентирующегося в мультимедиа. Очень четко объясните композитору, что вам нужно. Дайте ему образцы музыки с CD или из фильмов, эмоционально совпадающих с вашей игрой. Ваше сотрудничество будет плодотворнее, а музыка лучше, если вы ясно изложите композитору, чего вы с помощью его музыки хотите достичь в своей игре. Если же композитор не поймет вашего замысла, то вы только понапрасну потратите деньги, нервы и время.
Именно с помощью эффективного использования звуковых эффектов и музыки преодолевается тот самый психологический барьер, который мешает пользователю забыть, что он «всего лишь играет» и полностью погрузиться в созданный вами мир. Я думаю, что вы уже на собственном опыте убедились, насколько важны для восприятия игры удачное сочетание звуковых эффектов и музыки.
Разрешающая способность и количество цветов
Некоторые игры используют 16 цветов, другие - 256. В этой книге мы будем концентрироваться на разработке 256-цветпых игр. Разрешение экрана может быть 320х200, 320х240, 640х480 и выше. Применение высокой разрешающей способности связано с рядом проблем достижения необходимого быстродействия, если только вы не используете высокопроизводительный компьютер но принципы разработки графики в любом случае остаются одними и теми же.
Реализация многозадачности при помощи прерываний
При работе под операционной системой DOS, если и существует какой-нибудь способ, с помощью которого мы можем реализовать многозадачность, так это только использование механизма прерываний. Некоторые прерывания вырабатываются в результате внешних событий, в то время как другие евязаны с событиями внутренними. Для примера, давайте сначала рассмотрим, пожалуй самое популярное прерывание — прерывание от клавиатуры.
Всякий раз, когда происходит нажатие клавиши, ваша программа, что бы она при этом ни делала, останавливается и начинает работать процедура обслуживания прерываний клавиатуры. (Готов спорить, вы и не подозревали, что ваша программа останавливается при каждом нажатии на клавишу, однако это действительно так!) После окончания процедуры обслуживания прерывания управление снова передается вашей программе. Во время всего этого процесса ваша программа, данные и все остальное остается целым и невредимым. Для любой процедуры обслуживания прерываний это Правило Номер Один; без определенной цели ничего не должно уничтожаться. Так, например, если ваша процедура обслуживания прерывания использует для своей работы регистры процессора, вам первым делом следует сохранить содержимое этих регистров, затем осуществить обработку прерывания и снова восстановить содержимое регистров в точно таком же виде, каким оно было до прерывания.
Прежде чем мы начнем разбираться с основными принципами написания и установки обработчика прерываний, давайте взглянем, какие же прерывания есть у персонального компьютера. Посмотрите на таблицу 12.1.
Таблица 12.1. Прерывания ПК.
Номер
|
|
Адрес
|
|
Функция
|
|
0h
|
|
000-003h
|
|
Деление на ноль
|
|
1h
|
|
004-007h
|
|
Пошаговое выполнение
|
|
2h
|
|
008-00Bh
|
|
Немаскируемуе прерывание
|
|
3h
|
|
00C-00Fh
|
|
Точка останова
|
|
4h
|
|
010-013h
|
|
Переполнение
|
|
5h
|
|
014-017h
|
|
Печать содержимого экрана
|
|
6h
|
|
018-01Bh
|
|
Зарезервировано
|
|
7h
|
|
01C-01Fh
|
|
Зарезервировано
|
|
8h
|
|
020-023h
|
|
Таймер 18.2
|
|
9h
|
|
024-027h
|
|
Клавиатура
|
|
0Ah
|
|
028-02Bh
|
|
Зарезервировано
|
|
0Bh
|
|
02С-02Fh
|
|
RS-232 Порт 1
|
|
0Ch
|
|
030-033h
|
|
RS-232 Порт 0
|
|
0Dh
|
|
034-03Bh
|
|
Жесткий диск
|
|
0Eh
|
|
038-03Bh
|
|
Дискета
|
|
0Fh
|
|
03C-03Fh
|
|
Зарезервировано
|
|
10h
|
|
040-043h
|
|
Функция видеовывода
|
|
11h
|
|
044-047h
|
|
Проверка оборудования
|
|
12H
|
|
048-04ВН
|
|
Проверка памяти
|
|
13Н
|
|
04C-04FH
|
|
Функции ввода/вывода на дискету
|
|
14Н
|
|
050-053Н
|
|
Функции ввода/вывода последовательного порта
|
|
15Н
|
|
054-057Н
|
|
Функции ввода/вывода на кассетный магнитофон
|
|
16Н
|
|
058-05ВН
|
|
Функции ввода клавиатуры
|
|
17Н
|
|
05C-05FH
|
|
Функции вывода на принтер
|
|
18Н
|
|
060-063Н
|
|
Точка входа в ROM BIOS
|
|
19Н
|
|
064-067Н
|
|
Процесс загрузки
|
|
1АН
|
|
068-06ВН
|
|
Получение информации о времени
|
|
1ВН
|
|
06C-06FH
|
|
Управление прерыванием
|
|
1СН
|
|
070-073Н
|
|
Управление таймером
|
|
1DH
|
|
074-077Н
|
|
Таблица инициализации видеосистемы
|
|
1ЕН
|
|
078-07ВН
|
|
Таблица параметров дискеты
|
|
1FH
|
|
07C-07FH
|
|
Таблица графических символов
|
|
20Н
|
|
080-083Н
|
|
Завершение DOS
программы
|
|
21Н
|
|
084-087Н
|
|
Универсальные функции DOS
|
|
22Н
|
|
088-08ВН
|
|
Адрес завершения DOS
|
|
2ЗН
|
|
08C-08FH
|
|
Адрес обработчика Ctrl+Break
|
|
24Н
|
|
090-093Н
|
|
Адрес обработчика критических ошибок DOS
|
|
25Н
|
|
094-097Н
|
|
Абсолютное чтение с диска DOS
|
|
26Н
|
|
098-09ВН
|
|
Абсолютная запись на диск DOS
|
|
27H
|
|
09C-09FH
|
|
Установка резидентной программы DOS
|
|
28-3FH
|
|
0A0-0FFH
|
|
Зарезервировано для DOS
|
|
40-7FH
|
|
100-1FFH
|
|
Не используется
|
|
80-F0H
|
|
200-ЗСЗН
|
|
Зарезервировано для Бейсика
|
|
F1-FFH
|
|
3C4-3FFH
|
|
Не используется
|
|
<
Таблица 12.1 - это таблица векторов прерываний. Она занимает первые 1024 байт памяти каждого персонального компьютера. Всего в этой таблице 256 элементов, каждый из которых имеет размер 4 байта и представляет собой значение дальнего указателя на процедуру обслуживания прерывания. Как вы могли заметить, персональный компьютер не использует все 256 прерываний. Однако число задействованных прерываний постоянно растет.
Персональный компьютер поддерживает прерывания как на аппаратном, так и на программном уровне. Программные прерывания создаются с помощью расширенного набора инструкций процессора 80х86. Они были разработаны специально для того, чтобы дать возможность не только физическим устройствам мгновенно прерывать исполнение текущей программы. Большинство прерываний на персональном компьютере осуществляются программным путем. Однако некоторые осуществляются только с помощью аппаратуры (к ним относятся немаскируемые прерывания и прерывания от клавиатуры). С точки зрения программиста оба типа прерываний работают одинаково, поэтому нас это деление затрагивать не будет.
Внимание!
При использовании прерываний будьте очень осторожны: вы играете с огнем. Если вы допустите ошибку, компьютер может «зависнуть», что иногда приводит к потере важных данных. Будьте внимательны!
Итак, в соответствии с характером нашей игры мы должны выбрать нужные нам прерывания. Как их выбирать, еще не ясно, но мы разберемся с этим чуть позже. Затем нам нужно будет зарегистрировать (установить) свою собственную процедуру обработки прерываний. Вот, собственно, и все.
Единственное, чего нам не хватает для начала, это самой процедуры обработки прерываний, поэтому давайте разбираться, как она создается на языке Си.
Реализация отсектеля лучей
Я написал совершенно полную реализацию алгоритма отсечения лучей. Он закомментирован и даже содержит какую-то логику. Эта программа включена в комплекте поставки на дискете в файле RAY. С. Демонстрационная программа загружает двухмерную карту мира в виде ASCII-файла. На рисунке 6.30 приведена подобная карта. Она создается с помощью обычного текстового редактора.
Вся программа слишком длинна, чтобы включить ее в книгу, поэтому здесь приведена только ее самая интересная часть: процедура отсечения лучей. Попробуйте с ней разобраться и понять, что это и зачем так сделано.
|
|
1111111111111111
1 1
1 11111 1
1 1 1 1 1 1
1 1 1 1
1 1 11 1
1 1
1 1
1 1
1 11 111111 1
1 1 1 1
1 111 1 1
1 1 1 1
1 11111111 1 1
1 1
1111111111111111 |
|
|
Листинг 6.4. Процедура отсечения лучей (RAYLIST.C)
void Ray_Caster(long x, long y,long view_angle)
{
// эта функция выполняет расчет 320 лучей и строит игровой
// экран на основе их пересечений со стенами. Расчет производится
// таким образом, что все лучи отображаются на поле просмотра
// с углом 60 градусов
// после расчета траекторий лучей, рассчитываются координаты
// их пересечений со стенами. Координаты первой точки пересечения
// запоминаются. Ближайшие к игроку точки используются для
// построения битового образа изображения. Расстояния используются
// для определения высоты текстурных фрагментов и вертикальных линий
// Примечание: эта процедура использует функции стандартной
// библиотеки компилятора для работы с плавающей точкой
// (это работает медленно). Код не оптимизирован (еще медленнее),
// и в довершение всего обращается к функциям графической
// библиотеки компилятора фирмы Microsoft (что уж вовсе
// никуда не годится!) // Однако все это имеет определенную цель - легкость
// для понимания того, как работает процедура
int rcolor;
long xray=0, // счетчик вертикальных пересечений
yray=0, // счетчик горизонтальных пересечений
next у cell, // используются для вычисления
next_x cell, // номера следующей ячейки по ходу луча
cell_x, // координаты текущей ячейки
се11_у, // луча
x_bound, // следующие вертикальная
у_bound, // и горизонтальная точки пересечения
xb_save, // в этих переменных запоминаются
yb_save, // координаты точек пересечения
x_delta, // эти переменные показывают, на сколько
y_delta, // надо сместиться для перехода к следующей ячейке
ray, // текущий луч для отсечения
casting=2, // показывает компоненты Х и Y луча
x_hit_type, // координаты блока, с которым пересекся луч
y_hit_type, // используются при отрисовке
top, // верхняя и нижняя координаты области,
bottom; // отрисовываемой как стена (с помощью текстуры)
float xi, // используется для определения х- и у-пересечений
yi,
xi_save, // используется для сохранения
// точек пересечения Х и У
yi_save,
dist_x, // расстояние до х- и у-пересечениЙ
dist_у, // от точки просмотра scale;
// масштаб
//СЕКЦИЯ 1 ////////////////////////////////////////
// инициализация
// вычисляет начальный угол от игрока. Поле просмотра 60 градусов.
// Таким образом, рассматриваем только половину - 30 градусов
if ( (view_angle-=ANGLE_360) < 0)
// разворачиваем вектор направления взгляда
view_angle=ANGLE_360 + view_angle;
} // конец оператора if
// выбираем цвет для луча
rсо1оr=1 + rand()%14;
//СЕКЦИЯ 2 ////////////////////////////////////////
// цикл для всех 320 лучей
for (ray=0; ray<320; ray++)
// вычислить первое х-пересечение
if (view_angle >= ANGLE_0 && view_angle < ANGLE_180)
{
// вычислить первую линию, которая пересекается с лучом.
// Примечание: эта линия должна быть выше (впереди
// на игровом поле) игрока.
y_bound = CELL_Y_SIZE + CELL_Y_Sf2E * (у / CELL_Y_SI2E);
// вычислить смещение для перехода к следующей
// горизонтальной линии
y_delta = CELL_Y_SIZE; // размер ячейки по вертикали (ред.)
// основываясь на первой возможной горизонтальной линии отсечения,
// вычислить Х-пересечение и начать расчет
xi = inv_tan_table[view_angle] * (y_bound - у) + х;
// установить смещение
next_у_cell = 0;
} // конец обработки верхней половины плана
else
{ // вычислить первую горизонтальную линию, которая может
// пересекаться с лучом. Это будет позади игрока
y_bound = CELL_Y_SI2E * (у / CELL_Y_SIZE);
// вычислить смещение для следующей горизонтальной линии
y_delta = -CELL_Y_SIZE;
// основываясь на первой возможной горизонтальной линии отсечения,
// вычислить Х-пересечение и начать расчет
xi = inv_tan_table[view_angle] * (y_bound - у) + х;
next_y_cell = -1;
} // конец обработки нижней половины плана
//СЕКЦИЯ 3 ////////////////////////////////////////
// вычислить первое х-пересечение
if (view_angle < ANGLE_90 || view_angle >= ANGLE_270) {
// вычислить первую вертикальную линию, которая будет
// пересекаться с лучом. Она должна быть справа от игрока
x_bound = CELL_X_SIZE + CELL_X_SIZE * (х / CELL_X__SIZE);
// вычислить смещение
x_delta = CELL_X_SIZE;
// основываясь на первой возможной вертикальной линии отсечения,
// вычислить Y-пересечение и начать расчет
yi = tan_table[view_angle] * (x_bound - х) + у;
next_x_cell = 0;
} // конец обработки правой половины плана
else
{
// вычисляем первую вертикальную линию, которая может быть
// пересечена лучом. Она должна быть слева от игрока
x_bound = CELL_X_SIZE * (х / CELL_X_SIZE);
// вычислить расстояние до следующей вертикальной линии
x_delta = -CELL_X_SIZE;
// основываясь на первой возможной вертикальной линии отсечения,
// вычислить Y-пересечение
yi = tan_table[view_angle] * (x__bound - x) + у;
next_x_cell = -1;
}
// начать отсечение
casting =2; // два луча для одновременного отсечения
хrау = уrау = 0; // сбросить флаги пересечения
//СЕКЦИЯ 4 ////////////////////////////////////////
while(casting)
{
// продолжить отсечение лучей
if (xray!=INTERSECTION_FOUND)
{
// тест на совпадение луча с асимптотой
if (fabs (y_step[view_angle])==0)
xrау = INTERSECTION_FOUND;
casting--;
dist_x = 1e+8;
} // конец проверки на совпадение с асимптотой
// вычислить текущую позицию карты для проверки
сеll_х
= ( (x_bound+next_x_cell) / CELL_X_SIZE);
cell_y = (long)(yi / CELL_Y_SIZE) ;
// проверить, есть ли в этом месте блок
if ((x_hit_type = world[(WORLD_ROWS-1) - cell_y][cell_x])!=0)
{
// вычислить
расстояние
dist_x = (yi - y) * inv_sin__table[view angle];
yi_save == yi;
xb_save = x_bound;
// закончить х-отсечение
хrау = INTERSECTION_FOUND;
casting--;
} // конец проверки попадания луча на стену блока
else
{
// вычислить следующее Y-пересечение
yi += y_step[view_angle];
} //конец оператора else
} // конец проверки на отсечение по оси Х
//СЕКЦИЯ 5 ////////////////////////////////////////
if (yray!=INTERSECTION_FOUND)
{
// тест на попадание луча на асимптоту
if (fabs(x_step[view_angle])==0)
{
уrау = INTERSECTION_FOUND;
casting--;
dist_y=1e+8;
}
// вычислить
позицию карты
ceil_x = (long)(xi / CELL_X_SI2E);
cell_y = ( (y_bound + next_y_cell) / CELL_Y_SIZE) ;
// проверить, находится ли в этом месте блок
if ((y_hit_type = world[(WORLD_ROWS-1) - cell_y] [cell_x] ) !=0)
{
// вычислить
расстояние
dist_y = (xi - х) * inv_cos_table[view angle];
xi_save = xi;
yb_save = y_bound;
// закончить вычисление Y-пересечения
yray = INTERSECTION_FOUND;
casting--;
} // конец обработки попадания луча на блок
else
{
// вычислить следующее Х-пересечение
xi += x_step,[view_angle];
} // конец оператора
else
} // конец проверки на отсечение по оси У
// перейти к следующей точке пересечения
x_bound += x__delta;
y_bound += y_delta;
}
//СЕКЦИЯ 6 ////////////////////////////////////////
// выяснить, какая из стен ближе - вертикальная или горизонтальная
// и затем нарисовать ее
// Примечание: в дальнейшем мы заменим вертикальную линию на
// текстурный блок, пока же достаточно того, что есть
if (dist_x < dist_y)
{
sline(x,y,(long)xb_save,(long)yi_save, rcolor);
// вертикальная стена ближе горизонтальной
// вычислить масштаб и умножить на поправочный коэффициент
// для устранения сферических искажений
scale = cos_table[ray]*15000/(1e-10 + dist_x);
// вычислить координаты верха и низа
if ( (top = 100 - scale/2) < 1) top =1;
if ( (bottom = top+scale) > 200) bottom=200;
// нарисовать
фрагмент стены
if ( ((long)yi_save) % CELL_Y_SIZE <= 1 ) _setcolor(15);
else
_setcolor(10);
_moveto((int)(638-ray),(int)top);
_lineto((int)(638-ray),(int)bottom);
}
else // сначала надо нарисовать горизонтальную стену
{
sline(x,y,(long)xi_save,(long)yb_save,rcolor) ;
// вычислить
масштаб
scale = cos_table[ray]*15000/(le-10 + dist_y);
// вычислить координаты верха и низа
if ( (top = 100 - scale/2) < 1)
top = 1;
if ( (bottom = top+scale) > 200) bottom=200;
// нарисовать
фрагмент стены
if (((long)xi_save) % CELL_X_SIZE <= 1 )
_setcolor(15) ;
else
_setcolor(2);
_moveto((int)(638-ray),(int)top);
_lineto((int)(638-ray),(int)bottom) ;
} //конец оператора else
//СЕКЦИЯ 7 //////////////////////////////////////
//отсечь
следующий луч
if (++view_angle>=ANGLE_360)
{
// установить угол в 0
view_angle=0;
} // конец оператора if
} // конец цикла for
по лучам
} // конец функции
Реализация алгоритма отсечения лучей разделена на семь основных частей, чтобы легче было понять предназначение каждой из них.
§ Первая часть произведет инициализацию всех массивов и внутренних переменных. В этой части определяется направление взгляда игрока и на его основании вычисляется стартовый угол.
Также в этой секции выбирается случайное число, задающее цвет трассируемого луча;
§ Вторая и третья части вычисляют Х- и Y-пересечения текущего луча с периметром ячейки, в которой находится игрок. После того как найдено первое пересечение как с осью X, так и с осью Y, устанавливается значение нескольких переменных для определения траектории луча по отношению к осям координат. Информация, полученная в этих частях, используется в четвертой и пятой части;
§ Четвертая и пятая части продолжают проверку пересечений. Каждое из пересечений с координатной осью проверяется на пересечение с объектом. Если это происходит, то вычисляется дистанция и запоминается для дальнейшего использования. Эта информация может быть использована для тексту рирования объектов. Хотя рассмотренный нами трассировщик лучей и не делает этого, он предоставляет достаточно информации для текстурирования. Например, если луч пересекает середину стены блока, это означает, что мы должны вывести на экран 32-ю вертикальную полосу соответствующей текстуры. Более подробно этот вопрос будет рассмотрен позднее, в главе, посвященной описанию игры Warlock;
§ В шестой части заканчивается обработка луча. К этому моменту мы уже вычислили горизонтальные и вертикальные пересечения и запомнили расстояния до них. Следовательно, мы готовы нарисовать на экране соответствующую лучу вертикальную полосу. Для этого мы определяем, которое из пересечений находится ближе всего. Горизонтальная позиция для отрисовки соответствует номеру текущего луча и меняется в диапазоне от 0 до 319. Высота рисуемого фрагмента вычисляется на основании расстояния до игрока и с некоторыми корректировками для улучшения впечатления;
§ Седьмая часть увеличивает текущий угол и осуществляет переход к первой части. Цикл выполняется до тех пор, пока все 320 лучей не будут отсечены.
Вообще, то, что мы написали - неплохая штука. Она не рисует фактур и не просчитывает освещение, но это несложно реализовать. Теперь, когда у нас появилась работающая программа, есть смысл поговорить об оптимизации.
Реализм
Чтобы небо выглядело более реалистично, звезды, находящиеся на меньшем расстоянии от корабля, на котором, мы летим (от нашей виртуальной точки обзора), должны светить ярче. Я предлагаю следующий алгоритм создания трехмерного звездного неба:
1.
Для описания звезды создать структуру, содержащую информацию о ее расположении, цвете и скорости перемещения;
2. Создать массив звезд, расположенных по всему экрану и движущихся с разной скоростью;
3. Сделать так, чтобы звезды двигались из центра экрана к краю;
4. Сделать так, чтобы за каждый
5. Сделать так, чтобы, достигнув границы экрана, звезда появлялась вновь в иной случайной позиции и перемещалась с другой скоростью;
6. Нарисовать звезды;
7. Вновь вернуться к пункту 4.
Этот метод позволяет получить очень натурально выглядящие звезды и достаточно просто осуществляется. Однако и он тоже не лишен недостатков:
§ Во-первых, вы не можете повернуть и должны всегда двигаться вперед;
§ Во-вторых, начальная скорость звезды может быть слишком велика по отношению к скорости объекта, летящего по звездному небу.
Первая проблема действительно серьезная. .Если по сценарию игры вам требуется изменять направление полета, то надо воспользоваться каким-нибудь другим алгоритмом. Вторая же проблема, по моему мнению, чисто академическая. Кого волнует, что наш корабль летит со скоростью, во много раз превышающей световую? Это игра, и мы вольны строить такую вселенную, какая нам больше нравится!
Редактор поля WarEdit
Мы говорили о некоторых доступных инструментальных средствах, которые необходимы программистам игр и о том, как они используются. Теперь я хочу рассказать о тех инструментах, которые помогут в создании нашей собственной игры. Мы будем делать трехмерную игру под названием Warlock. Она будет очень проста, и я до сих пор даже не уверен в том, что в ней будут хоть какие-нибудь противники. Тем не менее, я собираюсь изготовить инструмент который позволит нам легко создавать уровни для игры.
Игровое пространство Warlock представляет собой трехмерный мир, с набором кубов, вертикальные грани которых разукрашены определенной фактурой. Общая площадь нашего мира приблизительно составляет 200х200 сторон кубов как показано па рисунке 15.2. Мы могли бы представить это пространство виде текстового файла и набить 40000 символов. Однако я не думаю, что сам , был бы этому рад. Вместо этого я решил создать простой редактор поля, который позволяет рисовать план игрового пространства, рассматриваемого сверху.
Редактор поля позволит нам легко создавать новые уровни и новые обстановки», в которых будут «жить» наши игровые объекты. Вы можете использовать мышь, чтобы рисовать поверхность, которая будет представлять собой полную игровую среду. Цвет, которым вы рисуете изображение, различает структуры и объекты, находящиеся в игровом пространстве. Существуют несколько управляющих функций редактирования поля. Коротко остановимся на их операциях.
Я решил представить игровое пространство, используя технику базовых элементов. Следовательно, все что мне нужно сделать, это создать двухмерную матрицу, представляющую собой игровое пространство, и заполнить ее целыми бедами, которые будут обозначать типы объектов, находящихся в соответствующих позициях. Объектами могут быть стены, «пища», «лекарство», монстры, «свитки» или двери.
Теперь, если стена или дверь помещены в определенную ячейку матрицы, они заполняют квадрат полностью. Однако есть и другие объекты, такие, наример, как «пища» или «свиток», которые имеют гораздо меньшие размеры, чем стена (которая в действительности представляет собой шестигранный куб; верх и низ его никогда не видны).
Помещая один из таких объектов на карту игрового поля, мы можем разместить его только где-то внутри квадрата. Более точно определить его положение мы не в состоянии. Когда программа встречает в базе данных такой объект, она случайным образом помещает его внутрь квадрата. Лучшим решением было бы создать редактор, который имеет более передовую структуру данных, с тем, чтобы мелкие объекты могли быть размещены более аккуратно. Однако того, что редактор делает сейчас, для наших целей вполне достаточно.
Вы можете создавать уровни, рисуя стены, двери и т. п. различными цветами, выбирая объект путем изменения цвета. Я применил цвета для пред. ставления объектов потому, что это самый легкий путь моделирования, хотя использование уменьшенных образов (иконок) объектов был бы, конечно намного предпочтительнее. Но при таком подходе сразу же возникает проблема которая заключается в том, что для представления матрицы размером 200х200 иконок понадобилось бы поле, во много раз превосходящее размеры экрана и для работы с ним потребовалось бы позаботиться о механизме прокрутки. Я не хотел моделировать все типы инструментов и смирился с цветами, как средством представления стен, дверей и всех прочих объектов. Тем не менее, чтобы видеть какой элемент игрового пространства вы устанавливаете, при выборе цвета соответствующий объект помещается в окно предварительного просмотра.
Признаюсь, что программа практически сырая. Я писал ее три дня. Изготовление настоящего редактора изображений с полным набором функций могло бы отнять месяцы кропотливого труда.
Редактор ID для создания игры DOOM отнял шесть месяцев на разработку и отладку. Но хотя изготовление хорошего инструмента и расход времени на него являются критическими величинами, WarEdit будет удовлетворять нашим потребностям. Он даст нам возможность рисовать уровни, размещать объекты в игровом пространстве и сохранять результат на диске.
В качестве последней особенности, WarEdit имеет окно детализации изображения, которое в увеличенном масштабе показывает область вокруг курсора.Это помогает размещению дверей и созданию тонких деталей.
Редакторы изображений
Графика и образы сегодняшних видеоигр должны быть максимально выразительными. Они должны быть объемными и выглядеть реалистично. Конечно, нет такого инструмента, который возмещает недостаток художественных способностей, а без настоящего художника ваша графика может выглядеть похожей на рисунки импрессионистов!
Можете использовать любую программу, какую пожелаете, но здесь приведен список тех минимальных средств, которыми она должна обладать:
§
Желательно, чтобы программа могла работать в любом графическом режиме, и обязательно — в режиме 13h (то есть 320х200х256);
§ Желательно, чтобы программа понимала различные форматы файлов как при чтении, так и при записи;
§ Интерфейс должен быть удобным, позволяющим рисовать образы в увеличенном масштабе (ZOOM), если в этом возникнет необходимость;
§ Цвет является важной деталью. Вам нужно иметь полный набор операций, управляющих цветовой палитрой;
§ В программе должна иметься возможность вывода изображений на принтер. Неплохо, если программа будет поддерживать цветные лазерные принтеры;
§ Редактор должен уметь выполнять различные геометрические преобразования изображений, такие как вращение, масштабирование, вырезание и растяжка;
§ Желательно, чтобы программа поддерживала возможность борьбы с неровностями контуров (сглаживание краев) и имела бы как можно больше цветовых эффектов;
Наконец, желательно, чтобы программа имела составные страницы, и чтобы, художник мог их отрезать и склеивать, а в дальнейшем получать окончательное изображение, оперируя изготовленными ранее графическими шаблонами.
Сегодня наблюдается тенденция постепенного вытеснения битовых графических редакторов иллюстративными, которые в основном оперируют объектами и векторным представлением образов. Независимо от ваших пристрастий, поиск удовлетворительного инструментария может отнять некоторое время, прежде чем вы найдете хорошую надежную программу. Я могу посоветовать вам приобрести такие продукты как Electronic Art's Deluxe Paint или Deluxe Animation, но, в крайнем случае, подойдет PC Paintbrush.
Регистры процессора
Регистры общего назначения. Данные регистры используются во время выполнения программ и во многих случая являются взаимозаменяемыми. Кроме того, каждый из них имеет определенное предназначение.
АХ - 16 бит, общего назначения, часто именуется аккумулятором;
ВХ - 16 бит, общего назначения и индексный;
СХ - 16 бит, общего назначения и счетчик;
DX - 16 бит, общего назначения;
ВР - 16 бит, общего назначения, используется для хранения смещения и индексов, часто называется регистром базы;
SI - 16 бит, общего назначения, используется в операциях с памятью (SI — source issue — регистр источника, используется для хранения смещения Источника при выполнении строковых команд);
DI - 16 бит, общего назначения, используется в операциях с памятью (DI - destination issue — регистр приемника, используется для хранения смещения пункта назначения при выполнении строковых команд).
Сегментные регистры. Данные регистры используются как указатели на сегменты. Сегмент - это блок размером в 64К, который предназначен для определенных целей: для хранения программного кода, данных и т. д.
DS - сегмент данных;
CS - сегмент кода;
ES - дополнительный сегмент;
SS - сегмент стека;
IP - счетчик.
Рисование линий
Нас совершенно не интересует написание функций для рисования линий с применением сложностей типа алгоритма Брезинхема. Более того, в наших играх вообще не будет линий, проходящих под произвольным углом. Напротив, у нас будет множество вертикальных линий и, может быть, немного горизонтальных. На самом деле, большинство игр вообще обходится без рисования линий, так как в них используются битовые массивы.
Мы напишем две функции. Одна из них рисует горизонтальные линии слева направо, а другая — вертикальные сверху вниз. Рисунок 5.4 показывает, как они выглядят в видеобуфере.
Поскольку горизонтальные линии рисовать легче, то с них мы и начнем. Как видно из рисунка 5.4, горизонтальную линию можно получить, заполняя ряд пикселей в матрице 320х200. Чтобы это проделать, мы должны найти начальный адрес строки и заполнить ее значениями пикселей от начальной до конечной позиции. Для этого стоит использовать функцию rnemset. Это один из самых быстрых способов. Листинг 5,6 содержит код такой функции.
Листинг 5.6. Рисование горизонтальной линии.
void H_Line (int х1, int x2, int y, unsigned int color)
{
// функция рисует горизонтальную линию, используя memset()
// x2 должно быть больше х1
_fmemset ((char far *) (video_buffer + ((у << 8) + (у
<<6)) + х1), color, x2 - х1 + 1);
} // конец функции
Следует кое-что запомнить:
§
Мы используем функцию _fmemset, поскольку она, в отличие от memset, корректно работает с дальними (FAR) указателями;
§ Мы вычисляем начальный адрес линии, и функция заполняет определенное количество байтов заданным значением цвета;
§ Количество байтов равно длине строки, которую мы вычисляем как разность между правым и левым концами линии. При этом нужно прибавить единицу, чтобы не потерять последнюю точку.
К сожалению, мы не можем использовать семейство функций memset для рисования вертикальных линий, поскольку они работают только с непрерывными областями памяти.
В случае вертикальных линий каждый следующий пиксель отстоит на 320 байт от предыдущего. Если вы находитесь в какой-то точке и хотите рисовать линию вниз, то вам надо прибавлять 320, а если линия рисует вверх, то надо вычитать 320 из текущего адреса. Таким образом, мы можем создать цикл, который увеличивает адрес на 320 и рисует вертикальную линию в виде пикселей сверху вниз. Листинг 5.7 демонстрирует код этой функции.
Листинг 5.7. Рисование вертикальной линии.
void V_Line(int yl,int y2,int x,unsigned int color)
{
//рисуем вертикальную линию (у2 больше yl)
unsigned int line offset,
index;
// вычисляем начальную позицию
line_offset = ((y1<<8) + (y1<<6)) + x;
for (index=0; index<=y2-y1; index++)
{
video_buffer[line_offset] = color;
line_offset+=320;
// переходим к следующей линии
} // конец цикла for
} // конец функции
Функция V Line несколько длиннее Н Line, поскольку она сама производит все адресные вычисления. Эта функция фантастически эффективна - вспомните пример использования сдвига вместо умножения (кстати, если вы до сих пор не поняли смысл двоичного сдвига, не отчаивайтесь - в восемнадцатой главе, «Техника оптимизации», мы это подробно изучим).
Прежде чем перейти к следующей теме, я хочу дать вам замечательную программу, которая создает новую палитру и выводит ее на экран, используя функции рисования вертикальных линий. В ней встречается уже известная функция Set_Mode() , описанная во второй главе, поэтому здесь я не включил ее исходный код. При желании вы можете взять его из второй главы. Более того, эта функция объявлена в программе как EXTERNAL, так что ее можно просто прилинковать. Листинг 5.8 содержит необходимый код программы Show_Palette.
Примечание
Я обнаружил некоторую проблему с чтением регистра палитры на VGA-картах. Похоже, что вы не всегда можете получить доступ к требуемому вам регистру. Это ошибка «железа», и она характерна для отдельных VGA-карт.
Я решил эту проблему довольно примитивным способом: просто дважды читаю каждый регистр. Вроде, мне это помогло. В своих программах вы можете попробовать включать дополнительные проверки и использовать разные функции для чтения регистров палитры в случае обнаружения ошибки. Во всяком случае, время, потраченное на решение данной задачи с лихвой окупится в процессе выполнения программы.
Листинг 5.8. Создание и отображение цветовой палитры (PALDEMO.C).
// ВКЛЮЧАЕМЫЕ ФАЙЛЫ ///////////////////////////////////////////////////////////////////////
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
// определения
/////////////////////////////////////
#define ROM_CHARSET_SEG 0xF000
#define ROM_CHAR_SET_OFF 0xFA6E
#define VGA256 0x13
#define TEXT_MODE 0х03
#define PALETTE_MASK ОхЗc6
#define PALETTE_REGISTER_RD.Ox3c7 #define PALETTE_REGISTER_WR 0x3c8
#define PALETTE_DATA 0x3c9
#define SCREEN_WIDTH (unsigned int)320
#define SCREEN_HEIGHT (unsigned int)200
// структуры данных////////////////////////////////////////
// структура, сохраняющая RGB
typedef struct RGB_color_typ
{
unsigned char red; // красный
компонент
0-63
unsigned char green; // зеленый
компонент
0-63
unsigned char blue; // синий
компонент
0-63
} RGB_Color, *RGB_color_ptr;
// ВНЕШНИЕ ФУНКЦИИ //////////////////////////////////
extern Set_Mode(int mode);
// ПРОТОТИПЫ //////////////////////////////////////////
void Set_Palette Register(int index, RGB_color_ptr color);
void Get_Palette_Register(int index, RGB_color_ptr color);
void Create_Cool__Palette();
void V_Line(int y1,int y2,int x,unsigned int color);
// ГЛОБДЛЬНЫЕ ПЕРЕМЕННЫЕ ////////////////////////////
// указатель на начало видеопамяти (для операций с байтами)
unsigned char far *video_buffer = (char far *)0xA0000000L;
// указатель на начало видеопамяти (для операций со словами)
unsigned int far *video_buffer_w= (int far *)0xA0000000L;
// ФУНКЦИИ //////////////////////////////////////////
void Set_Palette_Register (int index, RGB_color_ptr color)
{
// Эта функция устанавливает значение одного элемента таблицы
// цветов. Номер регистра задается переменной index, структура
// color содержит значения красной, зеленой и синей составляющих
// цвета
// указываем VGA карте, что мы будем обновлять содержимое
// регистра палитры
_outp(PALETTE_MASK,Oxff);
// какой из регистров мы хотим обновить?
_outp(PALETTE_REGISTER_WR, index) ;
// теперь обновляем RGB
_outp(PALETTE_DATA,color->red);
_outp(PALETTE_DATA,color->green);
_outp(PALETTE_DATA,color->blue);
} // конец функции
/////////////////////////////////////////////////////
void Get_Palette_Register(int index, RGB_color_ptr color)
{
// эта функция читает данные элемента таблицы цветов и помещает их
// в поля структуры color
// установить маску регистра палитры
_outp(PALETTE_MASK,Oxff);
// сообщаем VGA, какой из регистров мы будем читать
_outp(PALETTE_REGISTER_RD, index);
// читаем
данные
color->red = _inp(PALETTE_DATA);
color->green = _inp(PALETTE_DATA);
color->blue = _inp(PALETTE_DATA);
} // конец
функции /////////////////////////////////////////////////
void Create_Cool_Palette(void) {
// эта функция создает палитру, содержащую по 64 оттенка серого,
// красного, зеленого и синего цветов
RGB_color color;
int index;
// проходим по элементам таблицы цветов и создаем 4 банка
// по 64 элемента
for (index=0; index < 64; index++)
{
// оттенки
серого
color.red = index;
color.green = index;
color.blue = index;
Set_Palette_Register(index, (RGB_color_ptr)&color);
// оттенки красного
color.red = index;
color.green = 0;
color.blue = 0;
Set_Palette_Register(index+64, (RGB_color_ptr)&color) ;
// оттенки зеленого
color.red = 0;
color.green = index;
color.blue = 0;
Set_Palette_Register(index+128, (RGB_color_ptr)&color);
// оттенки синего
color.red = 0;
color.green = 0;
color.blue = index;
Set_Palette_Register(index+192, (RGB_color_ptr)&color);
} // конец цикла for
} // конец функции
///////////////////////////////////////////////////////
void V_Line(int y1,int y2,int x,unsigned int color)
{
// рисуем вертикальную линию у2 > yl
unsigned int line_offset, index;
// вычисляем начальную позицию
line_offset
= ((y1<<8) + (y1<<6)} + x;
for (index=0; index<=y2-y1; index++)
{
video_buffer[line_offset] = color;
line_offset+==320; // переходим к следующей линии
} // конец цикла for
} // конец функции
// ОСНОВНАЯ ПРОГРАММА /////////////////////////////////
void main(void)
{
int index;
RGB_color color,color_1;
// установить режим 320х200х256
Set_Mode(VGA256) ;
// создать палитру цветов
Create_Cool_Palette();
// рисуем по одной вертикальной линии для каждого цвета
for (index=0; index<320; index++) V_Line(0,199,index,index);
// ждем реакции пользователя
while(!kbhit())
{
Get_Palette_Register(0,(RGB_color_ptr)&color 1) ;
Get_Palette_Register(0,(RGB_color_ptr)&color_l);
for (index=0; index<=254; index++)
{
Get_Palette_Register(index+l,(RGB_color_ptr)&color);
Get_Palette__Register(index+l, (RGB_color_ptr)&color) ;
Set Palette Register(index,(RGB color_ptr)&color) ;
} // конец
цикла for
Set_Palette_Register(255,(RGB_color_ptr)&color_1);
} // конец цикла while
// переходим обратно в текстовый режим
Set_Mode(TEXT_MODE);
} // конец функции
Программа из Листинга 5.8 создает новую палитру, которая содержит 64 оттенка всех основных цветов, включая серый. Затем, она разделяет каждый цвет вертикальными линиями и после этого перемешивает их.
Вроде, хватит об этом. Теперь стоит поговорить о том, как целиком прочитать файл с образом. Начнем с формата PCX-файлов.
Рисование лучей
Очевидно, что лучи, которые мы отсекаем, на самом деле представляют собой линии. Они начинаются в точке зрения игрока, совпадающей с его позицией на двухмерной карте. Мы решили иметь поле просмотра равным 60°. Таким образом, нам нужно составить таблицу соответствия для всех возможных лучей, которые можно отсечь с любого угла просмотра. Эта таблица должна содержать значения наклонов всех возможных лучей по отношению к плану просмотра. Исходя из значения наклона, мы сможем произвести отсечение луча из точки наблюдения игрока.
Формула 6.1. Подсчет количества элементов в таблице значений наклонов.
Перед нами стоит вопрос — сколько элементов должно быть в таблице наклонов и как эти наклоны рассчитать? Для составления таблицы требуется знать, сколько в ней будет элементов. Когда игрок смотрит на мир, построенный отсечением лучей, то 320 лучей (количество горизонтальных точек экрана) вместе составят дугу в 60°. Таким образом, мы должны иметь таблицу с 1920 элементами или значениями наклонов. Это вычисляется по следующей формуле:
размер таблицы = ширина_экрана х 360 / поле_просмотра
В нашем случае ширина экрана составляет 320 пикселей, а поле_просмотра - 60 градусов, поэтому результат будет равен 320х360/60 = 1920.
Рисование точки
Однажды я сказал: «Дайте мне адрес видеобуфера и я переверну экран. ..». Это весьма правдивое высказывание. Во всех системах с отображением адресов видеопамяти на область адресов памяти обычной, как это делается в ПК; рендеринг был бы более простой и доступной вещью, если бы видеобуфер имел хоть каплю логики в своей организации. Собственно, организация и логика есть: видеобуфер - это один большой массив. И все.
Как мы узнали чуть раньше, для рисования точки нам достаточно вычислить адрес смещения относительно начала видеобуфера (А000:0000) и записать байт, отображающий ее цвет. Больше ничего делать не надо. Листинг 5.4 содержит фрагмент кода, который рисует точку определенного цвета с координатами Х и Y.
Листинг 5.4. Рисование точки в позиции (х,у).
void Plot_Pixel(int x,
int у, unsigned char color)
{
// эта функция отображает точку выбранного цвета. Каждая строка
// занимает 320 байт, поэтому для вычисления адреса надо умножить Y
// на 320 и прибавить значение Х
video_buffer[y*320+x]=color;
} // конец функции
Итак, рисование точки довольно просто. Я думал, все окажется сложнее, и поэтому так много написал об этом, но то, что мы использовали режим 13h, значительно упростило дело. Функция Plot_Pixel получилась простой, это всего одна строка кода. Тем не менее, давайте попробуем ее оптимизировать.
В книге есть целая глава, посвященная оптимизации, но эта единственная строка, содержащая операцию умножения, меня сводит с ума. Давайте избавимся от умножения. Кстати, возьмем за правило избегать операций умножения и вообще, откажемся от действий с плавающей запятой. Итак, посмотрим, что мы можем сделать с вычислением у х 320?
Вспомним, что мы используем двоичную арифметику и все числа в ПК также представлены в двоичном виде. Если вы берете двоичное число и сдвигаете его влево или вправо, это аналогично его умножению или делению на два.
Рисунок 5.3 поясняет это.
Поскольку операция сдвига выполняется примерно в 2-10 раз быстрее, чем умножение, то мы получим быстрые функции для рисования. Единственная сложность состоит в том, что число 320 — это не степень двух, но чтобы выйти из положения, мы применим маленькую хитрость. Представим выражение 320 х у, как 256 х, у + 64 х у. Листинг 5.5 показывает код для быстрого рисования точки.
Листинг 5.5. Программа быстрого рисования точки.
void Plot_Pixel_Fast ( int x, int y, unsigned char color )
{
// эта функция рисует точку несколько быстрее за счет замены
// операции умножения на сдвиг
// учитываем, что 320*у=256*у+64*у=у<<8+у<<б
video_buffer[((у<<8) +(у<<6)) + х ] = color;
} // конец функции
Эта функция работает примерно в два раза быстрее — вот что значит оптимизация. Позже мы научимся оптимизировать программы так, что парни из Microsoft перестанут нам верить.
Теперь, когда у нас есть функция рисования точки, надо дополнить нашу графическую библиотеку функцией рисования линии.
СЕКРЕТЫ VGA-КАРТ
Когда мы пишем видеоигры, то используем компьютер для создания миров и VGA-карту для рисования разнообразных объектов. В этом случае VGA-карта является «связующей нитью» между компьютером и экраном дисплея. В данной главе мы поговорим о следующем:
§
Обзор VGA-карт;
§ 256-цветный режим;
§ Конфигурация видеопамяти;
§ Таблицы цветов;
§ Переопределение цветовой палитры;
§ Цветовое вращение;
§ Рисование точек;
§ Рисование линий;
§ Файлы PCX;
§ Вывод битовых образов (бит-блиттинг);
§ Спрайты;
§ Вывод текста
§ Дублирующее буферизирование;
§ Синхронизация с вертикальной разверткой;
§ Игра Tombstone
Шаблонные мысли
На заре создания видеоигр программисты сталкивались с серьезными проблемами, пробуя нанести точки на экран. Они радовались по поводу удачной имитации тактики боя и написания набора алгоритмов, управляющих существами в их мире.
Увы, они думали, что могут имитировать разумное поведение с помощью шаблонов, то есть описывая рядом цифр траектории перемещения созданий. К примеру, в игре Galaxian маленькие космические корабли делают несколько кругов, стреляют в вас некоторое время, а затем возвращаются на прежнее место в стаю своих сородичей. Посмотрим на рисунок 13.1.
Когда они выполняют эти движения, то всего лишь следуют командам, заранее написанных шаблонов. Конструирование шаблонов - чрезвычайно легкое занятие и они нашли использование во многих сценариях. Как пример, используем шаблоны и случайные числа для моделирования «разума», который можно использовать, например, для управления небольшим космическим кораблем. Он пытается уклониться от вас, и в то же время пробует прицельно стрелять. То, что можно для этого сделать, показано в Алгоритме 13.3.
Алгоритм 13.3. Шаблоны со случайным выбором.
// Предположим, что pattern
- это массив, содержащий набор команд
// для реализации десяти различных шаблонов поведения
while(идет игра)
{
...код программы
// Проверяем, закончена ли обработка текущего шаблона
if (если обработка команд текущего шаблона закончена)
{
// Выбираем
новый шаблон
current_pattern = pattern[rand()%10];
позиция противника = старая позиция +следующий элемент текущего шаблона
Увеличиваем на единицу значение индекса элементов шаблона
...код программы
}
Алгоритм 13.3 кажется сложнее предыдущих Алгоритмов Преследования и Уклонения, но на самом деле это не так. В сущности:
§
Случайным образом выбирается некоторый шаблон;
§ Движение созданий в каждом проходе цикла изменяется в соответствии с направлением, указанным в шаблоне;
§ Затем мы переходим к следующему элементу шаблона.
Каждый шаблон может включать в себя произвольное количество элементов. Некоторые из них имеют 10 элементов, а иные — 1000.
Важно одно - когда «создание» исчерпывает набор команд, задаваемый одним шаблоном - оно переходит к другому.
Наконец, мы могли бы связать выбор случайного номера шаблона с действиями на основе некой другой логики - например, описываемой Алгоритмом 13.1.
Добавив к Алгоритму 13.3 последний шаг, мы придали ему некоторый аспект, который придал ему большую комплексность. Появилось подобие мыслительного процесса, состоящего из двух стадий. Случайное число подастся на вход селектора шаблонов, который затем выбирает новый шаблон для игры.
По аналогии с нервной системой, случайное число можно представить как нервный импульс, а выбранный шаблон действий - как реакцию на него. Посмотрите на рисунок 13.2, чтобы увидеть тот процесс, о котором я говорю.
Для усовершенствования тактики можно добавить измерение расстояния до игрока: если существо находится за пределами некоторого радиуса, используется Алгоритм Преследования, однако, когда оно оказывается достаточно близко, то начинает перемещаться по шаблонным траекториям, выбирая их случайным образом. Эта идея применена в Алгоритме 13.4.
Алгоритм 13.4. Преследование и Танец.
while(идет игра)
{
...код программы
if (игрок вне круга с радиусом 50 точек) then
преследуем его
else
выбираем случайный шаблон и реализуем его
...код программы
}
Для демонстрации использования шаблонов я переработал программу из Листинга 13.1 и привел ее в соответствие с Алгоритмом 13.4. Новая программа показана в Листинге 13.2. Когда враг приближается к игроку, он выбирает один из трех шаблонов и выполняет его до завершения. Далее, в зависимости от расстояния до игрока, противник либо преследует игрока, либо выбирает другой шаблон.
Листинг 13.2. Муха (FLY.C).
// ВКЛЮЧАЕМЫЕ ФАЙЛЫ //////////////////////////////////////////
#include
#include
#include
// ГЛОБАЛЬНЫЕ ПЕРЕМЕННЫЕ ////////////////////////////////////////
//указатель на системную переменную, содержащую
//значение таймера. Содержимое этой 32-битовой ячейки
//обновляется 18.2 раз в секунду
usigned int far *clock=(unsigned int far *)0x0000046C;
// Х- и Y- компоненты шаблонов траекторий, по которым будет
// двигаться "муха"
int patterns_x[3] [20]={1,1,1,1,1,2,2,-1,-2,-3,-1, 0,0,1,2,2,-2,-2,-1,0, 0,0,1,2,3,4,5,4,3,2,1,3,3,3,3, 2,1,-2,-2,-1, 0,-l,-2,-3,-3,-2,-2, 0,0,0,0,0,0,1,0,0,0,1,0,1};
int patterns_y[3] [20]={0,0,0,0,-1,-1,-1,-1,-1, 0,0,0,0,0,2,2,2,2,2,2, 1,1,1,1,1,1,2,2,2,2,2, 3,3,3,3,3,0,0,0,0, 1,1,1,2,2,-1,-1,-1,-2,-2, -1,-1,0,0,0,1,1,1,1,1};
/////////////////////////////////////////////
void Timer(int clicks)
{
// Эта функция использует значение таймера для формирования
// задержки. Необходимое время задержки задается в "тиках"
// интервалах в 1/18.2 сек. Переменная, содержащая 32-битовое
// текущее значение системного таймера, расположена
// по адресу 0000:0:46Ch
unsigned int now;
// получить текущее время
now = *clock;
// Ничего не делать до тех пор, пока значение таймера не
// увеличится на требуемое количество "тиков".
// Примечание: один "тик" соответствует примерно 55мс.
while(abs(*clock - now) < clicks) {}
} // конец функции Timer
// ОСНОВНАЯ ФУНКЦИЯ //////////////////////////////////////
void main(void)
{
int px=160,py=100, // начальные координаты игрока
ex=0,ey=0; // начальные координаты противника
int done=0, // флаг окончания работы программы
doing_pattern=0, // флаг выполнения команд шаблона
current_pattern, // номер выполняемого шаблона,
// принимает значение в интервале 0-2
pattern_element; / номер выполняемой команды шаблона
_setvideomode(_MRES256COLOR,) ;
printf(" The Fly - Q to Quit");
// главный игровой цикл
while(!done)
{
// очищаем точки
_setcolor(0) ;
_setpixel(px,py);
_setpixel(ex,ey) ;
// Перемещение игрока
if (kbhit())
{
// Определяем направление движения
switch(getch())
{
case 'u': // Вверх
{
py—2;.
} break;
case 'n': // Вниз
{
py+=2;
} break;
case 'j': // Вправо
{
px+=2 ;
} break;
case 'h': // Влево
{ px-=2 ;
} break;
case 'q':
{
done=1;
} break;
} // конец
оператора
switch
} // конец обработки нажатия клавиши
// Теперь перемещаем противника
// Начинается работа "мозга"
if (!doing_pattern)
{
if(px>ex) ex++;
if(px
if(py>ey) ey++;
if(py
// Теперь проверяем, не надо ли начать выполнять
// шаблон. Он начинает выполняться, если игрок
// оказывается в радиусе 50 точек от противника.
if (sqrt(.1+(рх-ех)*(рх-ех)+(ру-еу)*(ру-еу))<15)
{
// Даже не думайте использовать функцию sqrt в
// настоящей игре!
// Получаем случайный номер шаблона
curent_pattern = rand()%3;
// Переводим "мозг" в режим действий по шаблону
doing_pattern = 1;
pattern_element=0;
} // конец проверки на попадание игрока
// в "радиус действия"
} // конец действий, для случая, когда шаблон не выполнялся
else {
// перемещаем противника, используя следующий
// элемент
текущего шаблона
ex+=patterns_x[current_pattern][pattern_element];
ey+=patterns_y[current_pattern] [pattern_element];
// мы закончили обработку шаблона?
if (++pattern_element==20)
{
pattern_element = 0;
doing_pattern = 0;
} // конец проверки на окончание шаблона
} // конец оператора else
//конец работы, "мозга"
// рисуем точки
_setcolor(9);
_setpixel(px,py);
_setcolor(12);
_setpixel(ex,ey) ; // Немного
подождем...
Timer(1);
} // конец
цикла while
// восстановление начального видеорежима
_setvideomode(_DEFAULTMODE) ;
}// конец функции main
Когда вы запустите программу из Листинга 13.2, то поймете, почему я назвал ее "Муха". Точка, бегающая по экрану и в самом деле напоминает муху. Она приближается к вам и вдруг начинает быстро летать вокруг. И такое поведение персонажа воплощено всего в нескольких строках программы с применением описанных выше алгоритмов. (Гм, так как вы думаете, может быть люди - это и в самом деле комплекс действий и реакций?) Теперь рассмотрим случайные передвижения.
Синхронизация состояния ввода/вывода
Синхронизация состояния ввода/вывода является методом, при котором статус устройств ввода данных передается на другой ПК в реальном времени. Все, что игрок делает на одном компьютере, принимающая система воспринимает как входные данные, которые использует для корректировки в своем игрового пространстве поведения образа отдаленного игрока. Рисунок 14.8 пояснее сказанное.
Этот метод четко работает до тех пор, пока обе системы остаются синхронизированными и не происходит никаких случайных изменений игровой ситуации. Если же подобное произойдет, то другая машина не сможет «узнать» об этом, потому как данный способ не предназначен для передачи такого рода информации.
Если вам все же потребуется, чтобы происходили какие-то случайные события, вы должны воспользоваться первым методом, чтобы сообщить об изменениях другой машине. В дальнейшем мы объединим оба способа синхронизации вместе - это совершенно неизбежно.
Для синхронизации состояния ввода/вывода необходимо;
§
Опросить текущее состояние устройств ввода данных, будь то джойстик или клавиатура;
§ Объединить их вместе в пакет и послать через коммуникационный канал.
Термин пакет подразумевает объединение разносортной информации. Поэтому для пересылки пакетов мы должны принять ряд соглашений, чтобы последовательные коммуникационные системы «знали», что означает та или иная часть информации. Скажем, мы решили передать через коммуникационный канал положение ручки джойстика одновременно с состоянием его кнопок.
Формат пакета для передачи этих данных мог бы выглядеть примерно так, как это Показано в таблице 14.5.
Таблица 14.5. Образец пакета информационного пространства ввода/вывода.
№ байта Обозначение Смысл
0 J Установленное состояние джойстика
1 data_x Байт Х-координаты джойстика
2 data_y Байт Y-координаты джойстика
3 buttons Байт состояния кнопок
4 (period) Конец передачи
Составить такой пакет чрезвычайно просто. Не сложно и заставить программу передавать его на другую машину с частотой, необходимой для сохранения синхронизации. В то же самое время, другая машина могла бы предоставить в распоряжение первой собственный пакет состояния джойстика.
Здесь есть один не вполне очевидный нюанс. Дело в том, что оба компьютера в равной мере думают друг о друге как об удаленном игроке. (Есть в этом что-то от фантастических романов, описывающих путешествия в параллельные миры и во времени, - частенько путаешься и начинает болеть голова.)
Это все, что я хотел сказать по поводу синхронизации состояния ввода/вывода. Мы еще продолжим разговор о сохранении режима синхронизации, но сейчас нам нужно обсудить такую малоизученную область, как временная синхронизация.
Синхронизация вектора состояния
Реализовать синхронизацию вектора состояния несложно. Для этого достаточно непрерывно передавать другой машине данные о состоянии игрового пространства и принимать ответную информацию, чтобы скорректировать обстановку.
Давайте в качестве примера рассмотрим некоторую игру, в которой двое участников на разных компьютерах ведут дуэль с астероидами. Чтобы передать состояние одной машины на другую, мы должны учесть и местоположение, и скорость, и размер каждого астероида, а также не забыть передать и координаты самого игрока. Если игрок открыл огонь, мы должны передать соответствующее сообщение и об этом, а также описать атрибуты оружия. Кроме того, если в игровом пространстве на одной из машин появился новый объект, мы должны сообщить об этом другому компьютеру, чтобы и он создал аналогичный объект.
Таким образом, мы как бы делаем фотографии игрового пространства и постоянно передаем их на другую машину. Это нужно делать в разумном темпе, так, чтобы избежать всевозможных несогласованных ситуаций. Например, может же случиться так, что один игрок взрывает астероид в то время, как другой на него только нацелился. Как вы понимаете, в этом случае нужно удалить астероид из игрового пространства прежде, чем снаряд второго игрока достигнет уже не существующей цели. На рисунке 14.7 с некоторым преувелинием показано, что может произойти,. если система выйдет из состояния синхронизации.
Синхронизация вектора состояния работает прекрасно и совершенно надежно, потому что абсолютно все происходящее на одной машине передается на другую. Однако, как я подозреваю, это не так легко реализовать: ведь во внимание принимаются все возможные характеристики состояния игры, и в результате итоговая информация, передаваемая через коммуникационный канал, оказывается достаточно объемистой.
Следующий метод, о котором мы сейчас поговорим, более легок для понимания и называется синхронизацией состояния ввода/вывода.
Скан-коды
Давайте теперь поговорим о такой вещи как скан-коды. Если вы считаете, что при нажатии клавиши А обработчик клавиатуры также получает код символа А, то вы ошибаетесь. К сожалению, это не так. Обработчику посылается скан-код. Более того, он посылается дважды — при нажатии и отпускании клавиши. В видеоиграх нас будут интересовать не столько ASCII-коды, сколько нажатия клавиш A, S, Пробел, которые обычно отвечают за маневры, стрельбу и т. д. Таким образом, нам надо знать, как получить именно скан-коды. И это все, что требуется. В таблице 3.2 перечислены скан-коды клавиш.
Таблица 3.2. Таблица скан-кодов.
Клавиша
|
| Скан-код
|
| Клавиша
|
| Скан-код
|
| Клавиша
|
| Скан-код
|
| Клавиша
|
| Скан-код
|
|
Esc
|
| 1
|
| I
|
| 22
|
| Z
|
| 43
|
| F7
|
| 64
|
|
1
|
| 2
|
| O
|
| 23
|
| X
|
| 44
|
| F8
|
| 65
|
|
2
|
| 3
|
| P
|
| 24
|
| C
|
| 45
|
| F9
|
| 66
|
|
4
|
| 4
|
| [
|
| 25
|
| V
|
| 46
|
| F10
|
| 67
|
|
5
|
| 5
|
| ]
|
| 26
|
| B
|
| 47
|
| F11
|
| 133
|
|
6
|
| 6
|
| Enter
|
| 27
|
| N
|
| 48
|
| F12
|
| 134
|
|
7
|
| 7
|
| Ctrl
|
| 28
|
| M
|
| 49
|
| Num Lock
|
| 69
|
|
8
|
| 8
|
| A
|
| 29
|
| Запятая
|
| 50
|
| Scroll Lock
|
| 70
|
|
9
|
| 9
|
| S
|
| 30
|
| Точка
|
| 51
|
| Home
|
| 71
|
|
0
|
| 10
|
| D
|
| 31
|
| /
|
| 52
|
| Up
|
| 72
|
|
-
|
| 11
|
| F
|
| 32
|
| Правый Shift
|
| 53
|
| PgUp
|
| 73
|
|
=
|
| 12
|
| G
|
| 33
|
| Print Screen
|
| 54
|
| Серый -
|
| 74
|
|
Backspace
|
| 13
|
| H
|
| 34
|
| Alt
|
| 55
|
| Left
|
| 75
|
|
Tab
|
| 14
|
| J
|
| 35
|
| Пробел
|
| 56
|
| 5 на цифр. клав.
|
| 76
|
|
Q
|
| 15
|
| K
|
| 36
|
| Caps Lock
|
| 57
|
| Right
|
| 77
|
|
W
|
| 16
|
| L
|
| 37
|
| F1
|
| 58
|
| Серый +
|
| 78
|
|
E
|
| 17
|
| ;
|
| 38
|
| F2
|
| 59
|
| End
|
| 79
|
|
R
|
| 18
|
| Апостроф
|
| 39
|
| F3
|
| 60
|
| Down
|
| 80
|
|
T
|
| 19
|
| ~
|
| 40
|
| F4
|
| 61
|
| PgDn
|
| 81
|
|
Y
|
| 20
|
| Левый Shift
|
| 41
|
| F5
|
| 62
|
| Ins
|
| 82
|
|
U
|
| 21
|
| \
|
| 42
|
| F6
|
| 63
|
| Del
|
| 83
|
|
Если вы внимательно изучали таблицу, то должны, были заметить, что клавиши; имеющие двухсимвольную кодировку, обладают, тем не менее, только одним скан-кодом. Это происходит потому, что каждый скан-код может быть дополнен информацией о статусе клавиш. Кроме того, благодаря таблице 3.2, мы теперь сами можем по скан-коду определять код ASCII.
Случайные передвижения
Что я понимаю под случайным передвижением? Это выполнение персонажем какого-нибудь совершенно непредсказуемого действия. Нечто подобное мы уже сделали в программе из Листинга 13.2. Однако, мы можем расширить эту концепцию не только выбирая способ реагирования, но и определяя, нужна ли реакция как таковая. Почему бы нам не добавить еще один тип поведения нашей "Мухе" - случайное передвижение.
Случайные перемещения используются во многих видеоиграх как один из способов реагирования виртуального мира на действия игрока. Такой образ Действий можно назвать "неявной логикой". Неявная логика помогает принять Рвение при отсутствии части необходимой информации, или вообще информации как таковой.
В видеоиграх мы можем использовать случайные перемещения и неявную логику для выбора траекторий или действий наших созданий.
К примеру, когда вы сталкиваетесь с решением некоторого вопроса, такого как обход препятствия, возникшего на вашем пути, вы попытаетесь избежать встречи с ним, двигаясь направо или налево. Если все варианты равны между собой и одинаково возможны, решение, скорее, случайно и, следовательно является «неявным решением». Если бы кто-нибудь спросил вас, например почему вы решили обойти столб справа, а не слева, то вряд ли вы сумели бы внятно объяснить мотивы своего поведения. В другом случае мы можем использовать случайные переменные для выбора направления движения нашей маленькой «Мухи». Алгоритм 13.5 создает маленькую точку, которой теперь полагается летать, двигаясь в случайном направлении.
Алгоритм 13.5. Случайное передвижение.
while(идет игра)
{ ...код программы
if (перемещение точки по текущей траектории закончено) then
{
выбор новой траектории, иными словами, выбираем новый фактор преобразования координат
}
двигаем точку несколько раз по новой траектории
...код программы
}
Алгоритм 13.5 моделирует «грубый разум», который выбирает направление дальнейшего движения случайным образом. Напишем программу, которая создает одиноко летящую точку в пространстве с помощью этого алгоритма, Посмотрим, как это работает, на примере программы из Листинга 13.3.
Листинг 13.3. Одинокая муха. (DFLY.C).
// ВКЛЮЧАЕМЫЕ ФАЙЛЫ ////////////////////////////////////////
#include
#include
#include
// ГЛОБАЛЬНЫЕ ПЕРЕМЕННЫЕ ////////////////////////////////////////
// Указатель на системную переменную, содержащую
// значение таймера. Содержимое этой 32-битовой ячейки
// обновляется 18.2 раза в секунду
unsigned int far *clock=(unsigned int far *)0x0000046C;
//////////////////////////////////////////
void Timer(int clicks)
{
// эта функция использует значение таймера для формирования
// задержки. Необходимое время задержки задается в "тиках"
// интервалах в 1/18.2 сек. Переменная, содержащая 32-битовое
// текущее значение системного таймера расположена
// по
адресу 0000:046Ch
unsigned int now;
// получить текущее время
now = *clock;
// Ничего не делать до тех пор пока значение таймера не
// увеличится на требуемое количество "тиков".
// Примечание: один "тик" соответствует примерно 55 мсек.
while(abs(*clock - now) < clicks){}
} // конец функции Timer
// ОСНОВНАЯ ФУНКЦИЯ /////////////////////////////////////
void main(void)
{
int ex=160,ey=100; // начальная позиция "Мухи"
int curr_xv=1,curr_yv=0, // текущие факторы
// преобразования Координат.
clicks=0; // время, через которое
// "Муха" прекращает движение
//в произвольном направлении.
_setvideomode(_MRES256COLOR) ;
printf(" The Dumb Fly - Any Key to Quit");
//главный игровой цикл
while(!kbhit())
{
// очищаем точки на экране
_setcolor(0);
_setpixel(ex,ey); // перемещение "Мухи"
// "мозговой штурм"
// закончить ли движение в текущем направлении?
if (++clicks—20)
{
curr_xv = -5 + rand()%10; // от -5 до +5
curr_yv = -5 + rand()%10; // от -5 до +5
clicks=0;
} // конец задания нового направления
// перемещаем "Муху"
ex+=curr_xv;
ey+=curr_yv;
// убеждаемся, что "Муха" находится в пределах экрана
if (ex>319) ex=0; if (ех<0) ех=319;
if (ey>199) ey=0; if (еу<0) еу=199;
// конец "мозгового штурма"
// рисуем "Муху"
_setcolor(12);
_setpixel(ех,еу);
// небольшая задержка...
Timer(1);
} // конец
цикла while
_setvideomode(_DEFAULTMODE);
} // конец функции main
Думаю, после запуска программы вы согласитесь с тем, что мы, наконец получили все необходимое для моделирования летающего «разумного существа». Несколько простых правил, шаблоны и случайные числа помогли нам создать довольно правдоподобную имитацию полета насекомого. Это просто чудесно! Теперь настало время поговорить о конечных автоматах.
Смещение мозаичных изображений
Одной из проблем, возникающей при выводе повторяющегося смещаемого изображения, является большой объем памяти, требуемый для размещения битовых карт. Практически, размер никакого отдельного изображения в D0b не может превышать 64К. Этого достаточно для изображений площадью 320х200 или 640х100 пикселей (как я уже говорил, в режиме 13h каждый пиксель занимает один байт). Но даже если бы вам и представилась возможность иметь изображения больших размеров, вы все равно очень скоро исчерпали бы память, поскольку в вашем распоряжении имеется максимум 640К.
Не волнуйтесь: мы не будем здесь углубляться в изучение 16-битного кода, расширенной памяти или сегментации. Скажу только, что ограничения использования памяти неизменно сопутствуют старому доброму реальному режиму работы микропроцессора. Когда-нибудь это может измениться, но сейчас мы вынуждены примириться с таким положением вещей.
Существует довольно умное решение проблемы недостатка памяти — это применение мозаичных изображений. Вместо использования цельного образа, вы создаете виртуальную битовую карту, составленную из множества меньших картинок, как. из кирпичиков. Эти «кирпичики» могут быть рассмотрены как блоки здания, из которых составляется большее изображение. В основе этого технического приема лежит составление специальной справочной таблицы. Обычно она представляет собой массив, содержащий данные о расположении маленьких частей внутри большого виртуального изображения. Справочная таблица не требует много памяти (приблизительно один или два байта на элемент) и поэтому может иметь практически любую протяженность.
Представьте себе смещающееся виртуальное изображение, состоящее из 5400х12 «кирпичиков», каждый из которых имеет размер всего 16х16 пикселей. Это означает, что площадь виртуального изображения составит 86400х192 пикселя, что намного больше, максимальных допустимых размеров любого отдельного изображения.
Единственным ограничением этого метода является то обстоятельство, что размер цельного образа полностью зависит от размеров отдельных «кирпичиков».
Каждый из них должен быть небольшим и достаточно типовым, чтобы имелась возможность использования его в различных местах изображения. Тем не менее, «кирпичики» должны быть достаточно интересными, чтобы из них можно было сформировать привлекательное изображение.
По практическим соображениям кирпичики должны иметь ширину, равную степени числа 2. То есть их размер по горизонтали должен составлять 2, 4, 16 и так далее вплоть до 320 пикселей в режиме 13h. Важность этих ограничений вы поймете позже.
Одни из «кирпичиков», составляющих изображение, могут включать в себя «прозрачные» пиксели, а другие — нет. Последние наиболее пригодны для изображения дальних слоев, а также ближних планов, у которых отсутствуют «прозрачные» области, в то время как «кирпичики», содержащие «прозрачные» пиксели, служат для рисования частей изображения, имеющих пустоты. Но на самом деле «прозрачными» могут быть не только отдельные пиксели, но и целые "кирпичики", которые при выводе вообще пропускаются. Они могут располагаться в тех участках рисунка, где полностью отсутствует какое-либо изображение.
Помните, что вывод мозаичных слоев, включающих в себя "прозрачные" области, выполняется медленнее, потому что добавочные накладные расходы требуются для проверки существования «прозрачных» пикселей. В этой главея не показываю программу вывода «прозрачных» мозаичных слоев, но вы можете сделать это сами, применив технику вывода «прозрачных» пикселей. В своих собственных программах вы можете выбирать любые комбинации цельных и мозаичных слоев, «сплошных» или включающих в себя «прозрачные» пиксели, только не забывайте о различиях в способах их вывода.
При создании мозаичного изображения нужно учитывать тот факт, что на экране никогда не присутствует более двух «кирпичиков», выведенных не полностью. Рисунок 17.2 поясняет это свойство.
Это свойство несколько упрощает построение мозаичного изображения. Таким образом, этот процесс состоит из трех шагов:
1.
Рисование первого (возможно, не полностью выведенного) «кирпичика»;
2. Рисование нескольких (полностью выведенных) «кирпичиков»;
3. Рисование последнего (возможно, не полностью выведенного) «кирпичика» .
Также нам необходимо иметь в программе целочисленный счетчик, который будет указывать местоположение видимого экрана внутри виртуального изображения.
Программа из Листинга 17.6 (TILES.С) демонстрирует моделирование мозаичных слоев. Самые ближние слои состоят из нескольких «кирпичиков». Определение виртуального изображения сохранено в файле TILEMAP.DAT, который представляет собой обычный ASCII-файл и обрабатывается во время инициализации. Цифры в файле представляют собой закодированные имена PCX-файлов. Обратите внимание, что код 0 зарезервирован для «прозрачного кирпичика». Рисунок 17.3 показывает небольшой пример мозаичного изображения.
Важным отличием между этой программой и демонстрационной программой двойного параллакса в Листинге 17.3 является добавление функции DrawTile().
Эта подпрограмма изображает «кирпичик» в указанном месте экрана. Два аргумента offset и width определяют соответственно начальный столбец и ширину для вывода не полностью помещающихся на экране «кирпичиков».
Для частично выведенных кирпичиков:
§ offset -первый столбец, в котором будет нарисован «кирпичик»;
§ width - некоторое значение меньше ширины «кирпичика».
Для полностью выведенных «кирпичиков»:
§ offset - 0;
§ width - определяет ширину «кирпичика».
Программа из Листинга 17.6 также использует курсорные клавиши для управления движением и клавишу Esc для выхода. В демонстрационной программе на переднем плане появляется стена дома, составленная из отдельных «кирпичиков», а за ней видны хорошо известная горная гряда и линия неба на самом дальнем плане. Выполняется эта программа немного медленнее из-за использования в ней функции вывода мозаичного изображения, но только на пару кадров в секунду.
Листинг 17. 5 содержит файл заголовка для программы Построения мозаичного изображения, представленной в Листинге 17.6. В заголовке определены константы и прототипы функций для демонстрационной Программы.
Листинг 17.5. Заголовок мозаичного смещения (TILES.Н).
// Этот файл содержит определения, используемые программой
// прокрутки мозаичных изображений
#define NUM_TILES 17 // количество файлов,
// содержащих "кирпичики"
#define TILE_WIDTH 16 //ширина "кирпичиков"
#define TILE_HEIGHT 16 // высота "кирпичиков"
#define TILE_COLS 40 //ширина мозаичного изображения
#define TILE_ROWS 6 // высота мозаичного изображения
#define TILES_TOTAL (TILE_COLS*TILE_ROWS)
#define TILES_PER_ROW (VIEW_WIDTH/TILE_WIDTH)
#define shift 4
ftifdef _cplusplus extern "C"
{
#endif
void ReadTiles(void);
void FreeTiles(void);
void ReadTileMap(char *);
void DrawTile(char *,int,int,int,int);
void DrawTiles(int,int);
#ifdef __cplusplus
}
#endif
Поскольку программа из Листинга 17.6 практически повторяет 17.3, она приводится без комментариев.
Листинг 17.6. Демонстрационная программа мозаичного смещающегося слоя (TILES.C).
#include
#include
#include
#include
#include
#include "paral.h"
#include "tiles.h"
char *MemBuf,
*BackGroundBmp,
*ForeGroundBnip,
*VideoRam;
PcxFile pcx;
int volatile KeyScan;
int frames=0,
PrevMode;
int background,
foreground, position;
char *tiles[NUM_TILES+l];
int tilemap[TILES_TOTAL] ;
void interrupt (*OldInt9)(void);
//
//
int ReadPcxFile(char *filename,PcxFile *pcx)
{
long i;
int mode=NORMAL,nbytes;
char abyte,*p;
FILE *f;
f=fopen(filename,"rb");
if(f==NULL)
return PCX_NOFILE;
fread(&pcx->hdr,sizeof(PcxHeader),l,f);
pcx->width=1+pcx->hdr.xmax-pcx->hdr.xmin;
pcx->height=1+pcx->hdr.ymax-pcx->hdr.ymin;
pcx->imagebytes= ( unsigned int) (pcx->width*pcx->height) ;
if(pcx->imagebytes > PCX_MAX_SIZE)
return PCX_TOOBIG;
pcx->bitmap= (char*)malloc(pcx->imagebytes);
if(pcx->bitmap == NULL)
return PCX_NOMEM;
p=pcx->bitmap;
for(i=0;iimagebytes;i++)
{
if(mode == NORMAL)
{
abyte=fgetc(f);
if((unsigned char)abyte > Oxbf)
{ nbytes=abyte & 0x3f;
abyte=fgetc(f);
if(-—nbytes > 0) mode=RLE ;
}
}
else if(--nbytes ==0)
mode=NORMAL;
*p++=abyte;
}
fseek(f,-768L,SEEK_END);
fread(pcx->pal,768,1, f) ;
p=pcx->pal;
for(i=0;i<768;i++) *p++=*p>>2;
fclose(f) ;
return PCX_OK;
}
//
void _interrupt NewInt9(void) {
register char x;
KeyScan=inp(Ox60);
x=inp(0х61) ;
outp(0x61,(x|0x80));
outp(0x61,x) ;
outp(0х20,0х20);
if(KeyScan == RIGHT__ARROW__REL ||
KeyScan == LEFT__ARROW_REL)
KeyScan=0;
}
//
void RestoreKeyboard(void) {
_dos_setvect(KEYBOARD,OldInt9);
}
//
void InitKeyboard(void)
{
Oldlnt9=_dos_getvect(KEYBOARD) ;
_dos_setvect(KEYBOARD,Newlnt9);
}
//
void SetAllRgbPalettefchar *pal)
{
struct SREGS s;
union REGS r;
segread(&s) ;
s.es=FP_SEG((void far*)pal);
r.x.dx=FP_OFF((void far*)pal);
r.x.ax=0xl012;
r.x.bx=0;
r.x.cx=256;
int86x(0xl0,&r,&r,&s) ;
}
//
void InitVideo()
{
union REGS r;
r.h.ah=0x0f;
int86(0xl0,&r,&r); PrevMode=r.h.al;
r.x.ax=0xl3;
int86(0xl0,&r,&r);
VideoRam=MK_FP(0xa000,0);
}
//
void RestoreVideo()
{
union REGS r;
r.x.ax=PrevMode;
int86(0xl0,&r,&r) ;
}
//
int InitBitmaps()
{
int r;
background=foreground=l;
r=ReadPcxFile("backgrnd.pcx",&pcx) ;
if(r != PCX_OK) return FALSE;
BackGroundBnip=pcx.bitmap ;
SetAllRgbPalette(pcx.pal); ,
r=ReadPcxFile("foregrnd.pcx",&pcx);
if(r != PCX_OK) return FALSE;
ForeGroundBmp=pcx.bitmap;
MemBuf=malloc(MEMBLK) ;
if(MemBuf == NULL) return FALSE;
memset(MemBuf,0, MEMBLK) ;
return TRUE;
) //
void FreeMem()
{
free(MemBuf);
free(BackGroundBmp) ;
free(ForeGroundBmp) ;
FreeTiles(};
}
//
void DrawLayers()
{ OpaqueBlt(BackGroundBmp,0,100,background) ;
TransparentBIt(ForeGroundBmp,50,100,foreground) ;
DrawTiles(position,54) ;
}
//
void AnimLoop() {
while(KeyScan != ESC_PRESSED)
{
switch(KeyScan)
{ case RIGHT_ARROW_PRESSED:
position+=4;
if(position > TOTAL_SCROLL) {
position=TOTAL_SCROLL;
break;
}
background-=1;
if(background < 1)
background+=VIEW_WIDTH;
foreground-=2; if(foreground < 1)
foreground+=VIEW_WIDTH;
break;
case LEFT_ARROW_PRESSED:
position-=4;
if(position < 0) {
position=0;
break;
}
background+=1;
if(background > VIEW_WIDTH-1) background-=VIEW_WIDTH;
foreground+=2 ;
if (foreground > VIEW_WIDTH-1) foreground-=VIEW_WIDTH;
break;
default:
break;
} DrawLayers();
memcpy(VideoRam,MemBuf,MEMBLK) ;
frames++;
} }
//
void Initialize()
{
position=0;
InitVideo(} ;
InitKeyboard();
if(!InitBitmaps())
{
Cleanup();
printf("\nError loading bitmaps\n");
exit(l);
} ReadTileMap("tilemap.dat");
ReadTiles();
}
// void Cleanup() {
RestoreVideo() ;
RestoreKeyboard();
FreeMem();
}
void ReadTiles(void)
{
PcxFile pcx;
char buf[80];
int i,result;
tiles[0]=NULL;
for(i=l;i<=NUM_TILES;i++)
{
sprintf(buf,"t%d.pcx",i);
result=ReadPcxFile(buf,&pcx);
if(result != PCX_OK) ( printf("\ nerror reading file: %s\n",buf);
exit(1);
} tiles[i]=pcx.bitmap;
} }
void FreeTiles() { int i;
for(i=0;i
}
void ReadTileMap(char *filename)
{
int i;
FILE *f;
f=fopen(filename,"rt") ;
for (i=0; i
fscanf(f,"%d",&(tilemap[i])) ;
}
fclose(f);
}
//
void DrawTile(char *bmp,int x,int y,int offset, int width)
{
char *dest;
int i;
if(bmp == NULL) return;
dest=MemBuf+y*VIEW_WIDTH+x;
bmp+=offset;
for(i=0;i
memcpy(dest,bmp,width);
dest+=VIEW_WIDTH;
bmp+=TILE_WIDTH;
} }
//
void DrawTiles(int VirtualX,int Starty)
{
int i,x,index,offset,row,limit;
index=VirtualX>>SHIFT;
offset=VirtualX - (index<
limit=TILES_PER_ROW;
if(offset==0)
limit--;
for(row=Starty;row
row+=TILE_HEIGHT) {
x=TILE_WIDTH-of£set;
DrawTile(tiles[tilemap[index]],0,row,offset,
TILE_WIDTH-offset);
for(i=index+l;i
{
DrawTile(tiles [tilemap [i]], x, row, 0, TILE_WIDTH) ;
x+=TILE_WIDTH;
} DrawTile(tiles [tilemap[i] ] ,x, row, 0,offset);
index+=TILE_COLS;
}
}
//
int main() { clock_t begin,fini;
Initialize() ;
begin=clock();
AnimLoop() ;
fini=clock() ;
Cleanup() ;
printf("Frames: %d\nfps: %f\n",frames,
(float)CLK_TCK*frames/(fini-begin)) ;
return 0;
)
Собираем все вместе
Итак, мы рассмотрели основы работы с прерываниями, многозадачность и некоторые приемы программирования, которые несколько облегчают реализацию архитектуры компьютерной игры. Причем представленная здесь коллекция приемов программирования далеко не самая полная и лучшая. Мы просто обсудили эти вопросы для того, чтобы вы смогли представить себе различные способы организации игры. Впрочем, вскоре вы, наверняка, придумаете и свои Методы и трюки. Главная цель этой главы состоит в том, чтобы дать вам некую Справную точку и направление для ваших размышлений. Теперь я хочу просуммировать все полученные вами знания и разобрать несколько примеров применения рассмотренных нами методов. После этого для окончательного закрепления материала я приведу еще два примера программ.
В этой главе мы узнали о реализации многозадачности на персональных компыотерах с помощью прерываний. Прерывание — это всего-навсего мгновенная передача управления от выполняющейся программы к процедуре обслуживания прерывания (ISR), которая выполняет все действия, относящиеся к событию, вызвавшему прерывание. Прерывания полезны тем, что они позволяют реализовать на персональном компьютере некое подобие многозадачности. Более того, они могут быть использованы для управления задачами, зависящими от времени и событий, контролировать которые главной программе крайне затруднительно именно в силу самой природы этих задач. Обычно процедуры обслуживания прерываний используются для таких вещей как обработка ввода с клавиатуры или передача данных через последовательный порт. Однако мы можем использовать их и для других задач (работа со звуком, система ввода/вывода, обслуживающие функции и так далее).
Управление задачами, зависящими от времени и событий, осуществляется за счет подпрограмм обработки тех прерываний, которые происходят по наступлению определенного момента времени или в результате какого-либо события. В этом смысле очень полезным оказывается прерывание системного таймера 0х1С. Мы можем поместить адрес нашего обработчика в таблицу векторов по адресу, соответствующему прерыванию, которое генерируется при каждом системном «тике» (то есть 18.2 раза в секунду).
Это гарантирует нам, что независимо от загруженности системы, наша подпрограмма обработки прерываний будет вызываться всегда с неизменной частотой. Кроме того, мы знаем, что системный таймер можно запрограммировать и на другую частоту, например, 20Гц, 30Гц, 60Гц и так далее. Нам это может понадобиться, если мы собираемся использовать прерывание для чего-то, что требует более частого выполнения.
И, наконец, мы узнали, как использовать прерывания для изменения глобальных переменных, являющихся, по сути, сообщениями, на которые могут реагировать другие функции той же программы. Это может пригодиться в тех случаях, когда в игре надо выполнять критичные по времени исполнения операции, синхронизацию событий и тому подобные задачи.
Кроме того, мы обсудили как построить цикл игры. На самом деле это просто один из методов организации программы и входящих в нее функций таким образом, что все действия выполняются в определенной последовательности и в соответствии с определенной логикой. Мы узнали, что программа игры должна иметь раздел инициализации, за которым следует главный цикл событий. Внутри этого цикла осуществляется стирание графического изображения, получение входных данных от пользователя, выполнение необходимых графических преобразований, формирование и вывод на экран графического изображения. Конечно, порядок действий может быть несколько иным, как, впрочем, и входящие в него элементы, однако нам всегда следует создавать цикл событии, внутри которого эффективно и логически обоснованно делается все, что должно быть сделано.
Рассмотрев цикл игры, мы перешли к обсуждению достаточно отвлеченных на первый взгляд вопросов, относящихся к приемам программирования. Мы изучили два тесно связанных между собой типа функций, которые назвали автономными функциями, и функциями ответа. Эти приемы программирования были приведены в качестве иллюстрации, как много действий и процессов в компьютерной игре могут быть организованы независимо друг от друга и иметь свои собственные данные.
Компьютерные игры настолько сложны, что нам часто приходится писать функции, работающие без постоянного контроля со стороны главной программы. В этом нам помогают автономные функции. Кроме того мы рассмотрели еще и функции ответа. Эти функции «откликаются» на определенные события соответствующими действиями.
Обсуждение всех этих вопросов призвано помочь вам осознать, как писать программы, работающие в «реальном режиме времени». Иными словами, как организовать выполнение нужных действий таким образом, чтобы создать впечатление одновременности происходящего. В компьютерных играх без этого не обойтись.
Напоследок мы узнали, как перепрограммировать системные часы, реализованные микросхемой таймера 8253. Мы узнали, что в счетчике 0 хранится 16-разрядное значение, которое рассматривается как делитель поступающего сигнала с частотой 1.19318МГц. Результирующий сигнал используется для генерации «тиков», своего рода пульса, который инициирует прерывание по времени. Это прерывание может быть перехвачено с помощью вектора прерывания 0х1С. В ста процентах игр этот таймер перепрограммируется на более подходящую частоту вроде 30Гц или 60Гц. Таким образом, работа программы синхронизируется по новой базовой частоте. Помимо этого, входящая в состав игры подсистема вывода звуковых эффектов и подсистема ввода/вывода реа лизуются в виде фоновых задач, которые реализованы в виде процедур обслуживания таймерного прерывания 0х1С. Благодаря этому ввод/вывод и музыка исполняются с неизменной скоростью даже тогда, когда сама игра несколько замедляется вследствие выполнения вычислений или сложных графических эффектов.
Писать эту главу было непросто, поскольку в ней мне пришлось изложить вам очень много новых вещей. На самом деле, следует иметь в виду, что только опыт поможет вам разобраться с миллионами тонкостей, которые приходится учитывать при программировании компьютерных игр. Тем не менее, перед вами уже открылись новые возможности. Сейчас вы готовы мыслить по-новому и это Уже неплохо.
Как я и обещал, сейчас мы напишем еще несколько подпрограмм обслуживания прерываний и сделаем с их помощью несколько потрясающих вещей.
Содержание и общее настроение игры
Подумайте, что будет представлять собой ваша игра. Если это будет, например, космическое путешествие, то может потребоваться комбинация ярких и темных тонов: яркие будут представлять высокотехнологичные металлические конструкции, а более темные - звездные дали космического пространства. Средневековые сцены можно передать богатой палитрой земляных красок для отображения прошлого. Игры-ужастики могли бы, вероятно, потребовать темных, мрачных тонов. Если в вашей игре должны встретиться сцены как дневного, так и ночного освещения, это также необходимо учесть при разработке палитры.
Соединение через нуль-модем
Как я уже сказал в самом начале, у нас не хватит времени вникать во все тонкости использования модема. Существует слишком много тем, которых мы слегка коснулись, и все они имеет отношение к дизайну видеоигр. Что я намерен сделать вместо этого, так это создать коммуникационную систему, использующую соединение типа нуль-модем, в котором модем, как таковой, отсутствует. Нуль-модем — простое соединение, которое связывает два компьютера через последовательные порты. Рисунок 14,1 демонстрирует такое соединение.
Чтобы изготовить такое соединение, мы должны взять нуль-модемный кабель с надлежащими разъемами. Это может оказаться не слишком просто, но если вы будете осторожны, то заработает с первой попытки. Мы будем использовать только три типа разъема:
§
Линию передачи данных;
§ Линию приема данных;
§ Землю.
На рисунке 14.2 показано, как сделать нуль-модемный кабель для разных типов разъемов.
Если вы не хотите изготавливать нуль-модемный кабель, вы можете заплатить за него в любом из компьютерных магазинов. (Пожалуйста, не платите больше 15$. Я не могу спокойно смотреть, как люди платят за кусок провода и пластик даже 1.50$). Сейчас официальный нуль-модемный кабель использует более трех шин, которые я перечислил. Он задействует все шины, имитируя тем самым модемное соединение. Пока у нас есть TXD, RXD и земля, мы тоже при деле. (Дополнительные линии используются для аппаратного обеспечения «рукопожатия», но мы не будем ими пользоваться.)
Отлично, теперь у нас есть нуль-модемный кабель, и мы знаем, какие Регистры что делают в UART'e. Я думаю, что теперь самое время начать писать коммуникационное программное обеспечение.
Сохранение игры
Физики верят, что если бы они знали координаты и состояние всех частиц Вселенной, то смогли бы предсказывать будущее. Возможно, это и так. Перед нами же стоит задача попроще. Нам надо уметь записывать состояние нашей игры с тем, чтобы при необходимости его можно было восстановить.
Итак, проблема заключается в следующем: как запомнить игру так, чтобы в любой момент мы могли восстановить ее в прерванном месте? Ответ довольно Прост: надо создать список всех игровых объектов и факторов, которые могут измениться в течение игры. Затем создать файл определенного формата и записать в него всю эту информацию. Теперь, когда игрок хочет сохранить игру, мы просто записываем необходимую информацию на диск.
Для примера рассмотрим, как можно сохранить игру в DOOM или Wolfenstein 3-D:
§
В каждой игре присутствует множество неподвижных объектов. Поэтому их сохранять не надо. Однако во время сохранения игры нам необходимо помнить, что они существуют;
§ Также мы должны принимать во внимание настройки игры. Для каждой настройки мы должны запомнить копию ее структуры. Эти структуры должны содержать положение, состояние и т. д.;
§ Наконец, надо запомнить, какой инвентарь имеет при себе играющий на данный момент, а также каково его состояние (позиция, здоровье и т. д.), иными словами, то, что необходимо для правильного сохранения статуса игры.
Это все. Если кратко, мы делаем моментальный снимок нашего мира и представляем его в оцифрованном виде.
Соотношение пространства образов и пространства объектов
Алгоритм Z-буфера хорошо работает и может быть легко реализован. Единственная проблема состоит в том, что он работает на уровне пикселей и является алгоритмом обработки образа. Это значит, что он не рассматривает геометрических свойств объекта. Это требует наличия некоторого гибридного алгоритма для использования в специальных случаях. Такой алгоритм должен учитывать геометрические свойства объекта перед простым удалением невидимых поверхностей. Теперь давайте поговорим о том, как придать поверхности наших трехмерных объектов большую реалистичность.
Состояния бытия
Теперь я хотел бы обсудить одну философскую тему. Это и в самом деле философия, поскольку каждый из профессионалов делает это по-своему. Например, я пришел к выводу, что удобно включить в набор переменных, представляющих игровой объект, переменную «состояния». Эта переменная помогает оценивать объект с точки зрения того, должен ли он как-то меняться в зависимости от пространства или окружающей среды.
Например, мне всегда нравится в каждой структуре иметь переменную, которая описывает состояние объекта: жив, мертв, голоден и т, д. Эта переменная помогает в написании функций, создающих картину того, как объект погибает или воскресает вновь.
Поле состояния в структуре данных объекта надо иметь также и для того, чтобы использовать некоторые функции, изображающие объект в тот момент, когда он совсем здоров и когда он умирает. Эта особая информация берется из переменных состояния. Более подробно мы рассмотрим это в тринадцатой главе, «Искусственный интеллект». Однако сейчас нам надо знать, что система или объект в компьютерной игре (как и в реальной жизни) может проходить ряд состояний. Для того чтобы этого достичь, мы будем использовать переменные состояния в структуре данных каждого объекта.
Как пример использования состояния для оценки некоторых аспектов объекта игры давайте посмотрим на маленькую программу, создающую муравья. Муравей может ходить в разные стороны, пока не наткнется на стену или камень, после чего он выбирает новое направление (на север, юг, запад или восток). Так как четыре направления отслеживаются достаточно просто, мы можем воздержаться от использования переменных состояния. Однако движение на север, юг, запад и восток могут быть легко заменены такими эмоциями как чувство голода, усталость, счастье или гнев.
Для наглядности, используя наш метод имитации игровых объектов, я создал целый муравейник. Листинг 11.4 показывает эту программу.
Листинг 11.4. Муравейник (ANTS.С).
// ВКЛЮЧАЕМЫЕ ФАЙЛЫ
/////////////////////////////////////////
#include
#include
#include
#include
#include
#include
// определения /////////////////////////////////////////////
#define ANT_NORTH 0
#define ANT_EAST 1 #define ANT_SOUTH 2
#define ANT_WEST 3 #define NUM_ANTS 50
// структуры ////////////////////////////
// структура муравья
typedef struct ant_typ
{
int x,y; // позиция муравья
int state; // состояние муравья
unsigned char color; // цвет муравья: красный или зеленый
unsigned back_color; // фон
под муравьем
} ant, *ant_ptr;
// ГЛОБАЛЬНЫЕ
ПЕРЕМЕННЫЕ
///////////////////////////////////
unsigned char far *video_buffer: = (char far *)0xA0000000L;
// указатель
на видеобуфер
unsigned int far *clock = (unsigned int far *)0x0000046C;
// указатель на внутренний таймер
// наши маленькие муравьи
ant ants[NUM_ANTS];
// ФУНКЦИИ /////////////////////////////////////////////////
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*у + б4*у = у<<8 + у<<6
video_buffer[ ( (у<<8) + (у<<6) ) + х] = color;
} // конец Plot_Pixel_Fast ////////////////////////////////////////////////////////////
unsigned char Read_Pixel_Fast(int x,int у)
{
// читаем значение пикселя из видеобуфера
return (video_buffer [((у<<8) + (у<<6) ) + х]);
} // конец Read_Pixel_Fast
///////////////////////////////////////
void Draw_Ground(void)
{
int index;
// эта функция рисует разбросанные по экрану серые камешки
for (index=0; index<200; index++)
{
Plot_Pixel_Fast(rand()%320,rand()%200, 7 + rand()%2);
} // конец
цикла
} // конец Draw_Ground ///////////////////////////////////////////////
void Initialize_Ants(void)
{
int index;
for (index=0; index
{
// выбираем случайным образом начальную позицию, цвет и состояние
// для каждого муравья, а также определяем его фон
ants[index].х = rand(}%320;
ants[index].у = rand()%200;
ants[index].state = rand()%4;
if (rand()%2==1)
ants[index].color = 10;
else
ants[index].color = 12;
// сканирование фона
ants[index].back_color = Read_Pixel_Fast(ants[index].х, ants[index].y);
} // конец цикла
} // конец Initialize_Ants ////////////////////////////////////////////////////////////
void Erase_Ants(void)
{
int index;
// в цикле обрабатывается массив муравьев, все муравьи замещаются
// точками, цвета соответствующего фона
for (index=0; index
{
Plot_Pixel_Fast(ants[index].х, ants[index].y,
ants[index].back_color) ;
} // конец цикла
} // конец Erase Ants
////////////////////////////////////////////////////////////
void Move_Ants(void)
{
int index,rock;
//в цикле обрабатывается массив муравьев, каждый муравей перемещается
//в соответствии со своим состоянием
for (index=0; index
{
// каково состояние муравья?
switch(ants[index].state)
{
сазе ANT_NORTH:
{
ants[index].у—;
} break;
case ANT_SOUTH:
{
ants[index].y++;
} break;
case ANT_WEST:
{
ants[index].x--;
} break;
case ANT_EAST:
{
ants[index].x++;
} break;
} // конец оператора switch
// проверка, не столкнулся ли муравей
// с границами экрана или с камнем
if (ants[index].x > 319) ants[index].x = 0;
else if (ants[index].x <0)
ants[index].x = 319;
if (ants[index].у > 200)
ants[index].у = 200;
else if (ants[index].у <0)
ants[index].у = 199;
// здесь проверяем, не столкнулся ли муравей с камнем
rock = Read_Pixel_Fast(ants[index].x, ants[index].у);
if (rock)
{
// изменение состояния
ants[index].state =rand()%4; // выбор нового состояния
} // конец оператора if
} // конец цикла
} // конец Move_Ants
////////////////////////////////////////////////
void Behind_Ants(veid)
{ int index;
// в цикле обрабатывается массив муравьев,
// определяется цвет фона для каждого муравья
for (index=0; index
{ // читается пиксель и его значение сохраняется
// для
дальнейшего использования
ants[index].back_color = Read_Pixel_Fast(ants[index].x, ants[index].y) ;
} // конец
цикла
} // конец Behind_Ants ////////////////////////////////////////////////////////////
void Draw_Ants(void)
{
int index;
// в цикле обрабатывается массив муравьев, рисуется каждый
// муравей соответствующим цветом
for (index=0;index
{
Plot_Pixel_Fast(ants[index].x, ants[index].y, ants[index].color);
} // конец
цикла
} // конец Draw_Ants
// ОСНОВНАЯ ПРОГРАММА //////////////////////////////////////
void main
(void)
{
// установка видеорежима 320х200х256
_setvideomode(_MRES256COLOR);
_settextposition(2,0);
printf("Hit any key to exit.");
//построение игрового пространства
Draw_Ground() ;
/ создаем
муравьев
Initialize_Ants();
while(!kbhit())
{ // стираем всех муравьев
Erase_Ants
(};
// перемещаем всех муравьев
Move_Ants
();
// определяем фон под муравьями
Behind_Ants() ;
// рисуем всех муравьев
Draw_Ants
(),;
// немного подождем
Timer(2);
} // конец оператора while
// восстановление первоначального видеорежима
_setvideomode(_DEFAULTMODE);
} // конец функции main
Если вы запустите программу, то увидите кучу точек на экране. Красные и зеленые точки — это муравьи, а серые — это камни. Когда муравьи натыкаются на камни, они меняют направление движения. Эта программа показывает использование переменных состояния.Позже мы узнаем, как использовать переменные состояния для создания более сложных для вычисления структур, называемых состоянием машины.
Теперь изменим тему и поговорим о том, как выглядит игра с точки зрения игрока.
Создание бесшовных мозаичных изображений
К бесшовным относятся те изображения, которые можно разместить один за другим так, что не будет заметно, где заканчивается одно и начинается другое.
В некоторых играх изображения имеют только горизонтальную бесшовность (то есть они стыкуются друг с другом в непрерывную горизонтальную линию) Однако в некоторых играх требуется, чтобы изображения не имели швов не только по горизонтали, но и по вертикали. Это может понадобиться, наприме, в играх типа имитаторов полета для составления карты местности. Для создания пейзажа в таких случаях используются бесшовные «кирпичики», которые плавно переходят один в другой и по горизонтали и по вертикали.
На рисунке 16.17 показано, что получается, если заранее не предусмотреть возможности бесшовного соединения картинок.
Здесь наша горная цепь, отмасштабированная до размера 256х64 точек (размер четырех кирпичиков) помещена вслед за своей копией. Обратите внимание, что в месте соединения изображений появился хорошо заметный шов:рисунки не перетекают плавно один в другой. Этот шов необходимо отредактировать так, чтобы объединенные изображения выглядели как одна непрерывная картинка.
Существует два основных подхода к созданию бесшовных «кирпичиков». Во-первых, что вполне применимо к данному примеру, можно сделать зеркальную копию исходного рисунка. После этого изображения хорошо состыкуются друг с другом и их можно затем показать в игровом пространстве, как на рисунке 16.18.
Однако этот метод будет работать не для всех изображений. В тех случаях, когда вы хотите расположить рядом два совершенно одинаковых «кирпичика». потребуется отредактировать изображение в местах соединений. Подбирая цвета из области, окружающей шов, исправьте рисунок так, чтобы переход от одного изображения к другому не был заметен. На рисунке 16.19 показано, как выглядит соединение после модификации изображений.
Затем новое изображение, совпадающее по размерам с исходным «кирпичиком» (в нашем случае 256х64 пикселя), вырезается откуда-нибудь из середины общего изображения. Убедитесь, что отредактированная область попадает внутрь отрезаемой части. Полученный в итоге бесшовный «кирпичика показан на рисунке 16.20.
Приведенные выше иллюстрации
можно увидеть в цвете, просмотрев файл ЕХАМРЮ.РСХ.
Создание модели освещения
Сейчас мы уже изучили всю физику, которую надо знать для формирования хорошо выглядящей модели освещения. Мы знаем, что:
§
Чем дальше находится источник, тем меньше света он дает;
§ Если поверхность расположена под углом к источнику света, она отражает меньше света;
§ Если изменяется уровень рассеянного освещения, это сказывается на всех объектах в комнате. Все эти три фактора, взятые в совокупности, и формируют модель освещения.
Нам известны: угод, под которым виден источник света, уровень рассеянного освещения и расстояние до каждой из стен. Возникает вопрос: все ли это нам нужно? Может быть все, а может быть и нет. Это зависит от того, что называть реалистичным. Наша главная задача — сформировать затенение стен так, чтобы они выглядели реальными, но, в то же время, изображение стены формируется с помощью текстуры.
Существует два пути устранения этого противоречия.
Мы могли бы рассчитывать интенсивность каждого пикселя "на лету" и окрашивать его в соответствующий цвет. Единственная, возникающая при этом проблема, — недостаточное количество одновременно отображаемых цветов. Мы располагаем только 256 регистрами цвета, следовательно, одновременно на экране не может присутствовать больше 256 цветов. Это заставляет нас использовать цвет с большой осторожностью.
Тем не менее, это вполне работоспособный метод, дающий хорошие реэультаты. Я оставляю целиком на ваше усмотрение применимость к получающимся изображениям термина «реалистичные», но они достаточно хороши для видеоигр на ПК. Мы знаем, что должны изменять оттенок цвета стен в зависимости от угла, под которым они видны, и от их расстояния до игрока. Мы также знаем, что интенсивность окраски стен зависит от уровня рассеянного света. Теперь давайте сконцентрируемся на эффектах, связанных с углом обзора поверхности и расстоянием до нее.
Выполняя трассировку лучей, мы выводим на экран фрагмент текстуры.
Ее вид определяется содержимым регистров RGB в таблице цветов (см. пятую главу). Для выполнения операции затенения мы должны знать угол, образуемый поверхностью с направлением взгляда, и расстояние до нее. К счастью, нам известно и то и другое. Теперь осталось только выяснить, как использовать имеющуюся информацию для формирования затенения.
На практике нам нет необходимости использовать оба параметра в алгоритме затенения. Мы можем использовать или расстояние от стены до игрока или угол между направлением взгляда и поверхностью стены для получения вполне реалистичного изображения. Единственная проблема состоит в том, как получить все возможные эффекты затенения с помощью только 256 цветов?
Для решения этой проблемы мы можем создать палитру со следующими свойствами:
§ Первые 16 цветов - стандартные цвета EGA;
§ Дополнительные цвета помещаются в следующие 56 ячеек палитры. Это те цвета, с которыми мы будем работать, и единственные цвета, присутствующие в игре. Эти 56 цветов должны быть выбраны так, чтобы их хватило в качестве базовых для изображения всех игровых объектов. Более того, эти цвета будут самыми яркими в игре и они должны создаваться с учетом этого факта;
§ Теперь некоторая хитрость. Оставшиеся 184 цвета разбиваются на 3 банка по 56 цветов в каждом и банк из 16 цветов в конце палитры.
Три дополнительных банка по 56 цветов будут заполняться в процессе работы программы и использоваться механизмом формирования затенения. Дополнительный банк из 16 цветов используется для анимации палитры. Таким образом, палитра будет выглядеть как Представлено в таблице.
Таблица 6.1. Цветовая палитра для формирования затенения.
Регистры цвета
|
Функция
|
0 – 15
|
Базовые цвета EGA
|
16 - 71
|
Первичные цвета
|
72 – 127
|
Вторичные затененные производные первичных цветов
|
128-239
|
Третичные затененные производные
|
240 – 255
|
Дополнительные цвета для цветовой ротации и т.п.
|
<
Механизм затенения работает следующим образом: при отрисовке каждого вертикального фрагмента к значениям его пикселей добавляется некая константа. Например, если отрисовывается текстура из одного цвета с номером 16, то производные цвета будут иметь значения 16+56, 16+2х56 и 16+3х56 (или 72, 128 и 184 соответственно). В общем, механизм затенения берет за основу цвета текстуры и модифицирует их за счет использования других регистров цвета, номера которых определяются как сумма исходного цвета и константы. Если объект нарисован в первых 16 цветах, добавление константы не производится. Теперь мы имеем по четыре варианта каждого из 56 первичных цветов. Этого вполне достаточно для формирования реалистичного изображения. Три «затененных», банка цветов заполняются во время работы программы путем некоторого уменьшения интенсивности базовых цветов. Таким образом, всего получаются четыре банка из одних и тех же цветов с убывающей интенсивностью.
Теперь возникает вопрос, как пользоваться полученной таблицей и как готовить битовые карты? Когда вы будете рисовать свои битовые карты, рисуйте те объекты, которые впоследствии планируете затенять, с использованием 56 первичных цветов. Те объекты, которые затеняться не будут, рисуются в первых 16 цветах — они не затрагиваются механизмом затенения.
Кстати, так же создается и эффект локального освещения. Освещенные или светящиеся области рисуются с использованием первых 16 цветов и, соответственно, их цвет не изменяется при затенении.
Последняя проблема состоит в том, насколько мы должны затенять цвет. Как я уже говорил раньше, мы можем использовать или угол между поверхностью и лучом зрения или расстояние до игрока. Я рекомендую попробовать оба варианта и выбрать тот, который вам больше понравится. Что бы вы ни использовали, эта величина должна быть разбита на зоны. Если величина попадает в первую зону, используйте первую производную цвета, во второй зоне — вторую и т. д. Например, вы можете решить, что если угол составляет меньше 20°, то используется первая производная, если он оказывается в диапазоне 20-300 — вторая и т.
д.
Та же логика может быть применена и к расстоянию. Надо только учитывать, что интенсивность цвета падает нелинейно. В качестве примера можно привести разбивку расстояния на зоны, представленную в таблице 6.2.
Таблица 6.2.
Дистанция (условных ед.)
|
Банк цвета
|
0-20
|
Банк 0(цвета 16 – 71)
|
20-50
|
Банк 1(цвета 72 – 127)
|
50-100
|
Банк 2(цвета 128 – 183)
|
100-бесконечность
|
Банк 3(цвета 184 – 239)
|
Аналогичная таблица может быть создана и для углов. К настоящему моменту мы еще не готовы писать программу, реализующую механизм затенения, так как для этого требуется очень мощная оптимизация. Однако я надеюсь, что вы поняли идею.
Коротко повторим: цветовая палитра разбивается на шесть областей.
§ Первая и последняя области содержат цвета, не используемые при затенении;
§ Четыре средних области представляют собой банки по 56 цветов и каждый из этих банков представляет собой производные одних и тех же первичных цветов.
Когда изображение выводится на экран, каждый пиксель рисуется наиболее ярким цветом (банк 0), к которому добавляется некоторая константа, для формирования требуемой производной. Эта константа зависит от модели освещения, определяемой либо расстоянием от объекта до игрока, либо углом, под которым игрок смотрит на поверхность.
Вот и все.
Создание пользовательского интерфейса с системой распознавания голоса
Я бы хотел немного остановиться на новых формах пользовательского интерфейса, базирующихся на звуке. Сейчас уже очевидно, что мы в состоянии использовать системы распознавания голоса в разработке интерфейса программы. Я бы хотел дать несколько общих советов тем, кто собирается включить распознавание голоса в свои прикладные программы.
Управление голосом является наиболее естественным способом общения с компьютером. Почему же до сих пор это не очень получается? Большинство современных голосовых систем пассивны; они работают на заднем плане, пытаясь угадать, что же вы хотели им сказать. Проявившись же, они стирают файл или делают что-нибудь похожее по идиотизму.
Большинство скажет, что проблема в самих программах распознавания голоса. Но это не так. Проблема в дизайне пользовательского интерфейса.
Интерфейс с распознаванием голоса должен быть активным. Он должен переспрашивать пользователя при сомнениях и подтверждать получение команды. Он так же должен персонифицироваться через технику общения и используемые выражения.
Попробуйте пообщаться с приятелем, который никогда не отвечает - у вас появится ощущение, что вас игнорируют. Как минимум, вам нужно иногда услышать «Да-а» или «Ага», чтобы знать, что вас слушают. Или возьмите другой пример. Отец просит ребенка что-то сделать, а в ответ не слышит:
«Хорошо, папа». В таком случае папа обычно начинает злиться и кричать: «Эй, ты меня слышишь?!» (так, по крайней мере, происходит в моем доме.) При разговоре нам необходима ответная реакция, чтобы знать, что нас поняли. Интерфейс с распознаванием голоса должен использовать реплики типа: «Конечно», «Хорошо, шеф», «Простите?»
Несколько подобных фраз могут сделать интерфейс более естественным и позволят нам управлять сложными системами набором нескольких простых голосовых команд. Сегодня не существует реальной проблемы с системами распознавания голоса. Обычно они распознают отдельные слова достаточно аккуратно. Существующие коммерческие программы позволяют нам совершать очень сложные действия, используя несколько меню и пиктограмм.
Хорошо разработанная система с голосовым меню может быть не менее мощной и быть гораздо привлекательнее в использований.
И, наконец, последнее замечание о голосовом взаимодействии с компыотером. Говорящие машины заслужили плохую репутацию в начале 80-х из-за японских говорящих автомобилей: «Дверь открыта! Дверь открыта!». Все их ненавидели. Люди гордились тем, что им удалось «сломать» свои новые машины и выключить надоедливый голос. Заметьте, что проблема состояла не в плохом произношении машины, а в плохом пользовательском интерфейсе. Многих из нас еще в детстве научили, что не следует заговаривать с кем-либо, пока к вам не обратились. То же относится и к говорящим машинам. Если машина надоедает вам, прерывает ход ваших мыслей и мешает тому, что вы сейчас делаете, это очень раздражает. Однако если вы можете о чем-либо спросить машину и получить от нее ответ, это очень классная машина. Если машина хочет сообщить вам что-то действительно важное, она не должна врываться без приглашения, прерывая ваши мысли или разговор. Нет, она должна вежливо сказать «Гхм» или «Простите» либо просто прочистить свое электронное горло. Когда вы найдете время ответить и скажете: «Да, Саймон», компьютер может выдать накопившуюся информацию. Если системы с распознаванием голоса не являются достаточно естественными, они практически бесполезны. Мы будем продолжать использовать клавиатуру и мышь там, где это необходимо, но системы с распознаванием голоса должны стать более общительными, интерактивными и, самое главное, естественными.
Для персонификации машины одинаково подходит и использование разговорного языка, и акцент, и юмор. Обычно юмора боятся, вдруг кто-то подумает, что программа несерье;й1ая. Но кому понравится в очередной раз слушать идеальный, безжизненный голос «голосовой почты». Вместо этого, ваш компьютер мог бы сказать; «ОК, босс, все что пожелаете!» или «Конечно, Пап, нет проблем!» или «Инициализирую последовательность сохранения файла, доволен?» Конечно, это только примеры, и их не надо понимать буквально, просто они показывают, что общение с компьютером тоже может быть веселым.А ведь сделать общение с компьютером более естественным, простым, менее пугающим — и есть основная цель хорошо продуманного интерфейса.
В заключение, я хочу сказать, что приведенные здесь примеры представлены только для подстегиваиия вашей изобретательности. При разработке голосового интерфейса, просто задайте себе один вопрос: «Как бы это выглядело, если б я разговаривал с живым человеком?» Затем, учитывая технические ограничения, сделайте нечто максимально похожее на живую речь. Помните, что хотя машина может «слышать» лишь ограниченное число слов, «сказать» же она может все, что угодно. Составьте простой набор команд, и сделайте его минимально подверженным ошибкам. Старайтесь по возможности учитывать пожелания пользователя. Пусть ваши голосовые ответы варьируются и, самое главное, будут естественными.
Создание съемочного мини-павильона
Вам необходимо:
§ Во-первых, чистое, просторное место, чтобы установить видеокамеру и подставку. Расстояние между ними должно быть не менее четырех шагов. Это позволит вам поэкспериментировать с фокусом;
§ Во-вторых, я считаю, что комната для съемок должна быть по возможности одноцветной. Я не советую вам снимать в комнате с розовыми и голубыми стенами. Больше всего в данной ситуации подойдут белые стены и темный потолок;
§ Затем сделайте небольшую площадку для фотографии, фон которой вы сможете менять так, как это показано на рисунке 8.11. Я вырезал два кусочка оргстекла и установил их под прямым углом друг к другу. Затем я использовал цветную бумагу, для создания пола и фона для объекта.
Фон
В кино используется голубой фон, что позволяет отфильтровать синие тона в изображении. Хотя этот метод широко известен, у меня возникли с ним проблемы. В конце концов, я вынужден был использовать черный экран. Синий же фон имеет смысл использовать, если вы будете снимать черные объекты. Затем я подбирал освещение до тех пор, пока изображение объекта не получилось максимально контрастным по отношению к фону.
Платформа
Раз уж у вас есть камера и импровизированная недорогая студия, то вы должны сделать для своего объекта подходящую платформу. Она не должна портить изображение и впоследствии легко должна удаляться из полученной картинки. Вначале я использовал прозрачную подставку из оргстекла, но быстро убедился, что отражения на ее поверхности создают причудливые световые эффекты. Тогда я приклеил с нижней стороны подставки бумагу. Затем я проделал маленькое отверстие снизу моей модельки и закрепил ее на платформе. Получилось совсем неплохо. Правда, я мог поворачивать модель только вокруг одной оси и не мог наклонить ее. Однако, меня это вполне устраивало, так как мне и не требовались такие сложные ракурсы.
Освещение
Теперь поговорим об освещении. Когда вы начинаете оцифровывать видеоизображение, то быстро понимаете, насколько важно хорошее освещение и как малейшее его изменение может повлиять на внешний вид объекта. В результате я остановился на галогеновых лампах и вспышке, которая играла роль точечного источника света (вспышка имитировала солнце).
Создание студии
Сделать миниатюрную студию удивительно трудно, если вы не имеете для этого остаточных возможностей. Если вы, как и я, живете в обычной квартире, то сразу же обнаружите, что в ней нет удобного места для того, чтобы пилить, cверлить или рисовать. Хотя, как я уже говорил выше, мне хватило для создания студии лишь нескольких листов оргстекла, соединенных под прямым углом.
Если "вы не очень любите мастерить, можете пойти и купить несколько листов белой, черной и синей конструкторской бумаги. Я думаю, что стоит поэкспериментировать со всеми тремя цветами и выбрать для себя лучший. Приклейте бумагу к стене и поставьте вашу модельку перед ней на пол на какую-нибудь подставку. Для начала это вполне подойдет, но будет немного неудобно, потому что вам придется много ползать по полу на коленях.
Я думаю, что окончательно студия должна выглядеть так, как это показано на рисунке 8.12. Это должен быть стол того же цвета, что и параболически изогнутый; экран фона. Вращающийся экран должен быть закреплен на шарнирах, чтобы вы могли изменять его наклон и положение вместе с наклоном и положением объекта. Более того, желательно иметь две камеры, расположенные под углом друг к другу, чтобы снимать объект одновременно в двух разных ракурсах.
Создание внешних ссылок
Когда вы пишите модуль на Си, в котором встречаются переменные или функции, определенные в других модулях, вы должны использовать ключевое слово EXTERN, сообщающее компилятору, что переменные или функции будут определены позже (па этапе компоновки). MASM 5.0 и более старшие версии также поддерживают эту возможность.
В наших ассемблерных функциях может понадобиться передать значение глобальной переменной обратно в вызывающую Си программу. В принципе, мы можем ее передавать как параметр каждый раз и не думать о внешних переменных. Но данный способ критичен по времени и весьма архаичен. В видеоиграх, как ни в каких других программах, мы должны использовать как можно больше глобальных переменных. Причина очевидна — скорость выполнения. Нет смысла терять драгоценные такты процессора на выполнение команд PUSH и POP в момент вызова функций.
Синтаксис директивы EXTRN следующий:
EXTRN symbol: type, symbol: type,...
где symbol — имя переменной, a type — ее размер (например, BYTE, WORD, DWORD).
Директива EXTRN разрешает разместить переменную в вашем Си-коде и получить к ней доступ через параметры. Это имеет и обратную сторону: переменная, обозначенная как EXTRN означает, что она занимает текущий сегмент данных, адресуемых через регистр DS. Если вы все будете делать в модели SMALL или MEDIUM, то не стоит беспокоиться, если же вы работаете в модели LARGE, то никто не гарантирует, что вы получите доступ к вашей глобальной переменной, используя текущее значение регистра DS. Чтобы избежать этого, всегда применяйте модели SMALL и MEDIUM.
Давайте для примера напишем процедуру, которая складывает два целых числа и помещает результат в третье. Фокус заключается в том, что все эти величины будут глобальными и по отношению к ассемблерной процедуре - внешними. Листинг 2.6 демонстрирует код Си для этой программы, а в Листинге 2.7-показана ее реализация на ассемблере.
Листинг 2.6. Си-часть примера.
#include
int first = 1, second = 2, third = 0;
// Это те числа, с которыми
// мы хотим работать
void main (void)
{
printf ("\nBefore adding third = %d\ third);
Add_Ext();
//вызываем ассемблерную процедуру
printf("\nAfter adding third = %d",third);
} // конец функции main
Листинг 2.7. Ассемблерная часть примера.
.MODEL MEDIUM ; будем использовать MEDIUM модель
EXTRN first:WORD, second:WORD, third:WORD
.CODE ; начало кодового сегмента
_Add_Ext PROC FAR ;процедура имеет тип FAR (дальняя)
mov AX, first ; помещаем первое число в аккумулятор
add AX, second ; прибавляем второе число
mov third, AX ; помещаем результат в переменную third
_Add_Ext ENDP ; конец процедуры
END ; конец кодового сегмента
Листинги 2.6 и 2.7 - это примеры использования внешних переменных first, second и third. Программа вызывает Add_Ext, которая складывает переменные first и second и сохраняет результат в third. Это подводит нас к теме возврата результатов обратно в программу на Си, которая вызывала ассемблерную процедуру.
Создание звездного неба с использованием целых чисел
Теперь поговорим о том, как можно создать звездное небо используя только целые числа. Мы могли бы для получения более точной картины использовать числа с плавающей запятой, такие, например, как 2,5. Однако мы знаем, что это снижает производительность системы. Нам же требуется создать и визуализировать 50-100 звезд как можно быстрее, чтобы остальное время работы процессора потратить на масштабирование спрайтов и развитие сюжета игры.
Если создавать звездное небо, привлекая только целые числа как значения скоростей звезд, то едва ли вы заметите количественную ошибку, обусловленную заменой чисел с плавающей запятой на целые.
Конечно, мы могли бы принять компромиссный вариант и использовать числа с фиксированной запятой, но в данном случае овчинка выделки не стоит. Я считаю, что при достаточно высоких скоростях звезд потеря дробной части числа несущественна. Наверное, я уже утомил вас, повторяя одно и то же: в компьютерных играх все и всегда должно быть реализовано самым простым и быстрым путем. С другой стороны, такое примитивное на первый взгляд оформление игры, как экран, усыпанный звездами, может отнять у программиста удивительно много времени и сил. Но не забывайте, что отличное оформление - залог того, что игрок погрузится в игру с головой!
Листинг 8.3 содержит полный текст программы, позволяющей перемещать космический корабль по трехмерному звездному небу (отметим, что сам звездолет находится в плоскости X-Z). Для перемещения корабля используется вспомогательная цифровая клавиатура. Клавиши Left и Right вращают корабль, а клавиши Up и Down замедляют и ускоряют его движение. Для выхода из .программы нажмите клавишу Q.
При компоновке исполняемого файла вы должны объединить модуль FINVYREN.C с графической библиотекой GRAPHICS.С.
Листинг 8.3. Последний Шаттл (FINVYREN.C).
// ВКЛЮЧАЕМЫЕ ФАЙЛЫ//////////////////////////////
#include
#include
#include
#include
#include
#include
#include
#include
#tinclude
#include
#include
#include
#include "grapics.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);
void Timer(int clicks);
void Init_Stars(void) ;
void Move_Stars(void);
void Draw_Stars(void) ;
// ОПРЕДЕЛЕНИЯ /////////////////////////////////////////////
#define NUM_STARS 50 // количество звезд на небе
#define MAX_SCALE 200 // максимальный размер спрайта
#define SPRITE_X_SIZE 80 // размеры текстуры спрайта
#define SPRITE_y_SIZE 48
// СТРУКТУРЫ /////////////////////////////////////////////// // это звезда
typedef struct star_typ
{
int x,y; // позиция звезды
int xv,yv; // скорость звезды
int xa,ya; // ускорение звезды
int color; // цвет звезды
int clock; // число "тиков", которое звезда существует
int acc_time; // количество "тиков" до ускорения звезды
int acc_count; // счетчик ускорения
} star, *star_ptr;
// ГЛОБАЛЬНЫЕ ПЕРЕМЕННЫЕ ///////////////////////////////////
unsigned int far *clock = (unsigned int far *)0х0000046CL;
// указатель на внутренний таймер 18.2 "тик"/с
sprite object;
// обобщенный спрайт, который содержит кадры корабля
pcx_picture text_cells; // файл PCX с кадрами корабля
int *scale_table_y[MAX_SCALE+l] ;
// таблица предварительно рассчитанных коэффициентов масштабирования
int far *scale_table_x[MAX_SCALE+l];
// таблица предварительно рассчитанных коэффициентов масштабирования
star star_field[NUM_STARSj; // звездное небо
// ФУНКЦИИ //////////////////////////////
void Timer(int clicks)
{ // эта функция использует внутренний таймер с частотой 18.2 "тик"/с
// 32- битовое значение этого таймера находится по адресу 0000:046Ch
unsigned int now;
// получаем текущее время
now = *clock;
// Ожидаем до истечения указанного периода времени.
// Заметьте, что каждый "тик" имеет длительность примерно 55 мс.
while(abs(*clock - now) < clicks){}
} // конец Timer
////////////////////////////////////
void Init_Stars(void)
{
// эта функция инициализирует структуру данных звезд
// при старте программы
int index,divisor;
for (index=0; index
{
star_field[index].x = 150 + rand() % 20;
star_field[indexl.у = 90 + rand() % 20;
if (rand()%2==1)
star_field[index].xv = -4 + -2 * rand() % 3;
else
star_field[index].xv = 4 + 2 * randO % 3;
if (rand()%2==1)
star_field[index].yv = -4 + -2 * rand() % 3;
else
star_field[index].yv = 4 + 2 * randO % 3;
divisor = 1 + rand()%3;
star_field[index].xa = star_field[index].xv/divisor;
star_field[index] .ya = star_field [index] .yv/divisor;
star_field[index].color = 7;
star_field[index].clock = 0;
star_field[index].acc_time = 1 + rand() % 3;
star_field[index].acc_count = 0;
} // конец цикла
} // конец Init Stars
////////////////////////////////////////////////////////////
void Move_Stars(void) {
// Эта функция перемещает звезды и проверяет, не вышла ли звезда
// за пределы экрана. Если да, звезда создается вновь.
int index,divisor;
for (index=0; index
{
star_field[index].x += star_field[index].xv;
star field[index].y += star_field[index].yv;
// проверка выхода звезды за пределы экрана
if(star_field[index].x>=SCREEN_WJDTH || star_field[index].x<0 ||
star_field[index].y>=SCREEN_HEIGHT || star_field[index].y<0)
{
// восстановление звезды
star_field[index].x = 150 + rand() % 20;
star_field[index].у = 90 + randf) % 20;
if (rand()%2==l)
star_field[index].xv = -4 + -2 * rand() % 3;
else
star_field[index] .xv = 4 + 2 * rand() % 3;
if (rand()%2==l)
star_field[index].yv = -4 + -2 * rand() % 3;
else
star_field[index].yv = 4 + 2 * rand() % 3;
divisor = 1 + rand()%3;
star_field[index].xa = star_field[index].xv/divisor;
star_field [index] .ya = star_field.[index] .yv/divisor;
star_field[index].color = 7;
star_field[index].clock = 0;
star_field[index].acc_time = 1 + rand() % 3;
star field[index].ace_count = 0;
} // конец оператора if
// не пора ли ускорить движение звезды
if (++star_field[index].acc_count==star_field[index].асc_time)
{
// обнуляем счетчик
star_field[indexl.acc_count=0;
// ускоряем
star_field[index].xv += star field[index].xa;
star_field[index].yv += star_field[index].ya;
} // конец оператора if
//не пора ли изменить цвет звезды
if (++star_field[index].clock > 5)
{
star_field[index].color = 8;
} // конец оператора if (> 5)
else
if (star_field[index].clock > 10)
{
star_field[index].color =255;
} // конец оператора if (> 10)
else
if (star_field[index].clock> 25)
{
star_field[index].color = 255;
} // конец оператора if (> 25)
} // конец цикла
} // конец Move_Stars
//////////////////////////////////////////////
void Draw_Stars(void)
{
// эта функция рисует звезды в дублирующем буфере
int index;
for (index=0; index
{
Plot_Pixel_Fast_D(star_field[index].х,star_field[index].y, (unsigned char)star_field[index].color) ;
} // конец
цикла
} // конец Draw_Stars
////////////////////////////////////////////////////////////
void Create_Scale_Data_X (int scale, int far *row)
{
// эта функция масштабирует полосу текстуры всех возможных
// размеров и создает огромную таблицу соответствий
int х;
float x_scale_index=0, х_scale_step;
// рассчитываем шаг масштабирования или число исходных пикселей
// для отображения на результирующее изображение за цикл
x_scale_step = (float) (sprite_width)/(float)scale;
x_scale_index+=x_scale_step;
for (x=0; x
// помещаем данные в массив для последующего использования
row[x] = (int) (x_scale_index+.5);
if (row[x] > (SPRITE_X_SIZE-1)) row[x] = (SPRITE_X_SI2E-1);
// рассчитываем следующий индекс
х_scale_index+=x_scale_step;
} // конец
цикла
} // конец Create Scale_Data X
////////////////////////////////////////////////////////////
void Create_Scale_Data_Y(int scale, int *row)
{
// эта функция масштабирует полосу текстуры всех возможных
// размеров и создает огромную таблицу соответствий
int у;
float y_scale_index=0, y_scale step;
// рассчитываем шаг масштабирования или число исходных пикселей
// для отображения на результирующее изображение за цикл
y_scale_step = (float)(sprite_height)/(float)scale;
y_scale_index+=y_scale_step;
for (у=0; 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=1; scale<=MAX_SCALE; scale++)
{
scale_table_y[scale] = (int *)malloc(scale*sizeof(int)+1);
scale_table_x[scale] = (int far *)_fmalloc(scale*sizeof(int)+1);
} // конец цикла
// создаем Таблицу масштабирования для осей Х и У
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; // указатель на масштабированные
// по оси У данные (заметьте, что
// это ближний указатель)
int far *row_x;. // указатель на масштабированные
// по оси Х данные (заметьте, что
// это дальний указатель)
unsigned char pixel; // текущий
текстель
int x, // рабочие переменные
y,
column, work_offset, video_offset, video_start;
// если объект слишком мал, то и рисовать его не стоит
if (scale
// рассчитываем необходимые для масштабирования данные
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 (х=0; x
{
// пересчитываем адрес следующего столбца
video_offset = video_start + x;
// определяем, какой столбец должен быть отображен,
// исходя из индекса масштабирования по оси Х
column = row_x[x];
// Наконец рисуем столбец обычным образом
for (y=0; y
{
// проверка на "прозрачность"
pixel = work_sprite[work_offset+column];
if (pixel)
double_buffer[video_offset] = pixel;
// индекс следующей строки экрана и смещение в
// области хранения текстуры
video_offset
+= SCREENJHDTH;
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)
{
// корабль помещается в космическое пространство и игрок
// может перемещать его среди звезд
int done=0, // флаг
выхода
scale=64,
direction=6; // направление корабля (текущий кадр)
float sсale_distance = 24000,
view distance
= 256,
// произвольные константы для согласования масштабирования
// плоской текстуры в трассированном пространстве
х=0, // позиция корабля в пространстве
у=0,
z=1024,
xv=0,zv=0, // скорость корабля в плоскости Х-Z
angle=180,
// угол поворота корабля
ship_speed=10; // величина скорости корабля
// установка видеорежима 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();
// инициализация звездного неба
Init_Stars() ;
// установка направления и скорости корабля
angle=direction*30+90;
xv = (float)(ship_speed*cos(3.14159*angle/180));
zv = (float)(ship_speed*sin(3.14159*angle/180));
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)Sobject,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_pfcr) stext_cells,
(sprite_ptr)&object,7,1,2);
PCX_Grap_Bitmap((pcx picture ptr)&text cells,
(sprite_ptr)Sobject,8,2,2);
PCX_Grap_Bitmap((pcx_picture_ptr)&text_cells,
(sprite_ptr)&object,9,0,3);
PCX_Grap_Bitmap((pcx_picture_ptr)&text_dells,
(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 '4' // повернуть корабль влево
{
if (++direction==12)
{
direction==0;
} // конец оператора if
} break;
case'6': // повернуть корабль вправо
{
if (--direction < 0)
{
direction=11;
} // конец оператора if
} break;
case '8' : // ускорить корабль
{
if (++ship_speed > 20) ship_speed=20;
} break; case
'2': // замедлить корабль
{
if (--ship_speed < 0)
ship_speed=0;
} break;
case 'q': // выход из программы
{
done=l;
} break;
default: break;
} // конец
оператора
switch
// векторы направления и скорости
angle=direction*30+90;
xv = (float)(ship_speed*cos(3.14159*angle/180));
zv = (float)(ship_speed*sin(3.14159*angle/180)};
} // конец оператора if
// переместить корабль
x+=xv;
z+=zv;
// ограничить по ближней плоскости
if (z<256)
Z=256;
// рассчитать размер изображения
scale = (int)( scale_distance/z );
// на основании размера изображения
// рассчитать проекции Х и Y
object.х = (int) ((float)x*view_distance / (float)z) + 160 - (scale>>1);
object.у = 100 - (((int) ( (float) y*view_distance / (float)z) + (scale>>1)) );
// ограничить рамками экрана if
(object.х < 0)
object.х
= 0;
else if (object.x+scale >= SCREEN_WIDTH)
object.x = SCREEN_WIDTH-scale;
if (object.у < 0) object.у
= 0;
else
if (object.y+scale >= SCREEN_HEIGHT)
object.у
= SCREEN_HEIGHT-scale;
// выбрать
нужный кадр
object.curr_frame = direction;
// очистить дублирующий буфер
Clear_Double_Buffer() ;
Move_Stars();
Draw_Stars();
// масштабировать спрайт до нужного размера
Scale_Sprite((sprite_ptr)&object,scale) ;
Show_Double_Buffer(double_buffer);
// вывести на экран информацию для игрока
_settextposition(23,0) ;
printf("Position=(%4.2f,%4.2f,%4.2f) ",x,y,z) ;
// немного подождать
Timer(1) ;
} // конец while
// удалить файл
PCX PCX_Delete((pcx_picture_ptr)&text_cells) ;
// восстановить текстовый режим
_setvideomode(_DEFAULTMODE);
} // конец функции main
Как и прежде, в теле самой программы располагаются только вызовы функций. Это сделано для облегчения понимания работы алгоритма. При написании реальной, а не демонстрационной программы вам не обязательно поступать так же. Если вы сделаете свою программу чересчур модульной, то может оказаться, что время, потраченное на вызов некоторых функций, станет больше времени их выполнения!
Теперь, после того, как мы узнали математику и механику вывода на экран преобразований трехмерных спрайтов, давайте посмотрим, как я создавал артинки для программ этой главы.
Специальные эффекты
Под специальными эффектами обычно понимают такие зрительные эффекты, которые, выходят за рамки обычного и хорошо запоминаются. Современные компьютерные игры все больше и больше походят на настоящее кино и имеют весьма впечатляющую графику. Однако всегда можно найти что-то еще, что заставит игрока удивится. Поэтому попытаемся добиться от него возгласа:
"Ого!!!"
Специальные эффекты могут быть какими угодно, начиная от ярких взрывов и кончая сверхъестественными явлениями, психоделической графикой и всем-чем-хотите. Например, сейчас я работаю над компьютерной моделью системы частиц для того, чтобы монстр разлетался на биллионы маленьких атомов, несущихся каждый по своей орбите. Это должно выглядеть отлично! Я не уверен, что я сделаю это, но если мне это удастся, то это будет сводить с ума!
Специальные эффекты — это та область, в которой вы должны полностью Проявить себя, потому что это самая творческая часть любой компьютерной игры. Запомните одну вещь: в любой игре должен быть по-крайней мере один потрясающий воображение эффект, такой эффект, который игрок не скоро забудет! Итак, к делу, тем более что в этом разделе мы поговорим и о других важных вещах.
Способы воспроизведения звука
Рассмотрим способы, с помощью которых вы можете воспроизводить звук на персональных компьютерах.
Цифровой звук. Выпуск фирмой Creative Labs платы Sound Blaster заложил фундамент для использования оцифрованных звуков персональными компьютерами. Все последующие звуковые карты (такие как Covox Speech Thing, The Walt Disney Sound Source или Media Vision ProAudio Spectrum) поддерживали эту возможность. Цифровой звук позволяет вашей программе исполнять все, что может быть записано с помощью микрофона, включая звуковые эффекты, человеческую речь и музыку. Конечно, цифровой звук требует огромных объемов памяти и дискового пространства, но воспроизведения в играх звуковых эффектов и диалогов очень эффективно.
Цифровой звук - это как раз то, что вы слышите при воспроизведении музыкальных записей с компакт-дисков. Звук представляет собой волну определенной формы. Звук, воспроизводимый динамиком, — это, фактически, напряжение, подаваемое на катушку динамика и заставляющее мембрану двигаться вперед и назад в соответствии с силой сигнала. Движения мембраны динамика генерируют в воздухе звуковую волну. И наоборот, звуковая волна может быть переведена в цифровую форму, пугем преобразования уровня напряжения в числовые значения. Этот процесс называется аналого-цифровым преобразованием или АЦП. Форма звуковой волны приближенно описывается некоторым множеством повторяющихся выборок. Таким образом, звуковая волна, преобразованная в цифровую форму, имеет две важные характеристики: число использованных битов для представления данных и частоту, с которой звуковая волна была оцифрована. На стандартный звуковой компакт-диск музыка записывается с разрешением 16 бит и частотой 44КГц в стереозвучании. Это значит, что каждая выборка имеет 32 бита и выборки производятся 44 тысячи раз в секунду. Таким образом, одна секунда звука на компакт-диске требует 176000 байт. Одна минута - уже свыше 10 мегабайт!
Обычно компьютерные игры используют звуковые данные со значительно более низким разрешением, так как проигрывание цифрового звука с качеством компакт-диска потребует всех ресурсов машины.
Как правило, игры имеют дело с 8-битными выборками на частоте 11 КГц. Но даже на такой частоте совсем короткий звук легко займет всю память машины. Так что, хотя цифровой звук и является лучшим способом создания разнообразных звуковых эффектов, таких как выстрелы и взрывы, использовать его для музыкального оформления компьютерных игр чересчур накладно.
Другая проблема заключается в том, что, хотя оцифровка звука и позволяет проигрывать музыкальные записи, с ее помощью практически невозможно интерактивно управлять музыкальным фоном в соответствии с текущим контекстом игры. Для устройств MIDI такой проблемы не возникает, так как воспроизведение осуществляется под полным контролем со стороны программного обеспечения. Другими словами, вы можете в реальном времени посылать компьютеру эквивалент нотной записи.
Частотные синтезаторы. Самой первой популярной звуковой картой для персонального компьютера была плата Adiib Personal Music System. Она содержала синтезатор частотной модуляции Yamaha YM3812 (OPL2) FM. Э".э квазипрограммируемое устройство способно с помощью осцилляторов формировать вид звуковой волны, используя частотную модуляцию, генератор белого шума и операторы нарастания, удержания, затухания и освобождения звука. Это звучит замысловато не только на словах, но и на деле. Устройство феноменально сложно программируется. Даже приложив всю вашу изобретательность при программировании частотного синтезатора, вам вряд ли удастся достичь приемлемых результатов. К счастью, с появлением нового поколения MIDI-синтезаторов с волновыми таблицами прямое программирование частотных синтезаторов уходит в прошлое. Существует „много .систем, позволяющих YM3812 эмулировать устройство MIDI, избавляя программистов от необходимости работать с замысловатой логикой этого устройства.
MIDI. Спецификация интерфейса электромузыкальных инструментов (MIDI) - это международный стандарт «де-факто», который определяет последовательный интерфейс для объединения музыкальных синтезаторов, музыкальных инструментов и компьютеров.
MIDI поддерживается Ассоциацией производителей MIDI (Лос-Анджелес, Калифорния). Он опирается на аппаратное (каналы ввода/вывода и соединительные кабели) и программное обеспечение (тон, громкость и так далее). В соответствии со спецификацией, принимающее устройство в MIDI-системе должно интерпретировать музыкальные данные даже в том случае, если у передающего устройства нет возможности узнать, что способно делать принимающее устройство. Это чревато проблемами, если приемник не способен корректно интерпретировать данные. В обобщенном MIDI эта проблема решается предварительной идентификацией возможностей аппаратуры.
Все устройства обобщенного MIDI поддерживают звучание музыкальных и ударных инструментов, вместе со 128 звуковыми эффектами. Системы обобщенного MIDI поддерживают одновременное использование 16 MIDI-каналов с минимум 24 нотами в каждом и имеют определенный набор музыкальных контроллеров. Это означает, что в случае обобщенного MIDI передающее устройство знает, что ожидать от принимающего. Точно так же и файл, созданный одним устройством, соответствующим спецификациям обобщенного MIDI, распознается любым другим подобным устройством без потери нот или нарушения баланса инструментов при воспроизведении. Это, однако, справедливо только в теории. На практике же файл MIDI обычно имеет характеристики, существенно зависящие от конкретного устройства. Это обусловлено тем, что если сами инструменты и стандартизованы в обобщенном MIDI, то их баланс, тембр и качество — нет. Таким образом, каждый обобщенный MIDI-инструмент играет с разными характеристиками. При написании произведения MIDI для качественного исполнения на любом устройстве композитору не следует использовать специфические особенности конкретного синтезатора. Так как файлы MIDI обычно небольшие, то я, чтобы полностью использовать все возможности каждого синтезатора, обычно делаю различные версии для наиболее популярных MIDI-синтезаторов.
Большинство поставщиков придерживаются обобщенного стандарта MIDI, в том числе Roland (Roland Sound Canvas, Roland RAP-10), Creative Labs (AWE 32), Logitech- (Logitech SoundWave), Ensoniq (SoimdScape), Gravis (Gravis Ultrasound), Turtle (Turtle Beach Maui), Sierra Semiconductor (Aria).
Кроме того, устройства с частотными синтезаторами, такие как Sound Blaster, также могут эмулировать устройства MIDI с помощью набора драйверов типа MIDPAK. Видимо, в будущем вся интерактивная музыка будет придерживаться стандарта .MIDI. Благодаря этому вы сможете качественно воспроизводить с помощью разнообразных звуковых карт полнооркестровые произведения, созданные для вас профессиональными композиторами. Файлы данных MIDI небольшие, требуют сравнительно небольшого числа прерываний для обслуживания и не сильно загружают процессор. Большинство современных MIDI-синтезаторов используют алгоритмы. Алгоритмы — это оцифрованные записи реальной игры на разнообразных инструментах. Алгоритмы хранятся в постоянном запоминающем устройстве синтезатора. И когда MIDI-файл описывает, например, звучание пианино, то используется оцифрованный звук настоящего пианино. Благодаря этому мы получаем удивительно реалистично звучащую музыку. Полный набор алгоритмов, включающий в себя хоровые эффекты и эффекты реверберации, придаст вашей музыке дополнительный колорит.
MOD. Файлы MOD стали популярны благодаря компьютерам Amiga фирмы Commodore. Феноменальные для своего времени возможности этого игрового компьютера просто поражали. Аппаратное обеспечение этой машины поддерживало четыре канала цифрового звука. Многие создатели игр, сотрудничавшие с фирмой Amiga, хотели, чтобы в их играх звучала музыка. Поэтому они придумали схему, позволяющую получать полнооркестровую музыку, а файлы, содержащие звуковые данные, получили наименование MOD. Практически, этот метод базируется на программной эмуляции частотного синтеза с использованием алгоритмов (о них мы рассказывали выше в разделе MIDI). Надеюсь, вы еще помните, что алгоритмы содержат оцифрованную запись игры на разнообразных инструментах и хранятся в ПЗУ. Когда требуется воспроизвести звук пианино, то модулируется частота настоящей, исполненной на фортепиано, оцифрованной ноты. При использовании технологии MOD-файлов, делается все то же самое, только программным путем.
Это требует достаточного количества оперативной памяти для хранения оцифрованных звуковых эффектов и программного обеспечения для частотной модуляции в реальном времени. Такая задача отнимает много памяти и ресурсов процессора. Кроме того, написание музыки в виде MOD-файлов значительно более трудоемкая задача, чем создание композиций для MIDI.
Так что, хотя файлы MOD и являются наиболее полным, наиболее реалистичным методом создания музыки для простых устройств типа Sound Blaster, они все же не могут соревноваться с настоящим синтезатором MIDI. Кроме того, когда цифровой канал звуковой платы занят исполнением музыки, усложняется воспроизведение шумовых эффектов. Так как современный рынок движется в сторону MIDI-синтеза, наиболее разумным будет писать музыку в формате MIDI, а фоновые и шумовые эффекты воспроизводить с помощью оцифрованных звуков. Пакеты программ MIDPAK и DIGPAK как раз и предназначены для этих целей.
Красная книга компакт-дисков.
Одно из преимуществ дисководов CD-ROM, которые становятся все популярнее, состоит в том, что они могут проигрывать стандартные компакт-диски. Однако вы не можете сделать так, чтобы ваша игра одновременно и работала с диска CD-ROM и воспроизводила музыку оттуда же. Доступ к записанным на CD-ROM данным и к звуковым дорожкам является полностью взаимоисключающим. Также невозможно добиться плавной работы программы при постоянном переходе от данных к звуку и обратно. Однако многие разработчики любят помещать музыкальные фрагменты из игр на звуковые дорожки CD-ROM, и вы также можете взять это себе на вооружение.
Программное обеспечение цифрового микшироваиия.
За исключением Gravis Ultrasound и Creative Labs AWE32, почти все звуковые карты поддерживают только один канал для цифрового звука. Однако в интерактивной игре вам может потребоваться воспроизвести одновременно несколько звуков. Этого вы можете добиться, применяя программное обеспечение, которое осуществляет цифровое микширование. Так как звук аддитинен, этот процесс удивительно прост.
Все необходимые звуки, которые должны звучать в данный момент, складываются в буфере, отсекаются точки, вышедшие за установленные пределы, а затем результат передается звуковой карте. Некоторые инструментальные пакеты для разработки программного обеспечения включают поддержку программного цифрового микширования в свои программные интерфейсы.
Загружаемые алгоритмы инструментов и звуковых эффектов.
При использовании звуковых карт Gravis Ultrasound и Creative Labs AWE32 программа может загружать музыкальные инструменты или эффекты в память самой платы. После этого вы можете манипулировать ими, простоинициируя события MIDI. Это очень мощный метод, потому что он позволяет не только осуществлять поддержку многоканальности и использовать оригинальные инструменты и эффекты при низкой загрузке процессора и экономном расходовании памяти. Данный метод еще дает возможность манипулировать звуковыми эффектами в реальном времени, применяя сдвиг высоты звука, управление ударными инструментами и даже хоровые эффекты и эффекты реверберации.
Спрайты
Вы можете спросить: «Что такое спрайт?». Знаете, есть такой газированный напиток... Снова шучу. На самом деле спрайты - это такие маленькие объектики, которые находятся на игровом поле и могут двигаться. Этот термин
прижился с легкой руки программистов фирмы Atari и Apple в середине, 70-х годов. Теперь поговорим о спрайтах и их анимации. В будущем мы еще вернемся к этой теме в седьмой главе, «Продвинутая битовая графика и специальные эффекты. Именно с этой мыслью я создал несколько небольших спрайтов, которые мы будем использовать в дальнейшем.
Спрайты - это персонажи в играх для ПК, которые могут без труда перемещаться по экрану, изменять цвет и размер. Все это звучит как мечта программиста. Но надо помнить, что в IBM-совместимых компьютерах нет спрайтов! Во нормальных компьютерах существует аппаратная поддержка спрайтов. Такие машины как Atari, Amiga, Commodore и последние модели Apple имеют эту возможность, а вот ПК - нет. Поэтому мы вынуждены делать это самостоятельно.
М-да. Нам будет чем заняться.
Конечно, мы не станем заниматься разработкой аппаратной поддержки спрайтов. Все, что нам нужно, это понять, каким образом помещать образ на экран, сохраняя при этом возможность его перемещений и видоизменений. Поскольку спрайт — это довольно сложный объект, то стоит подумать о том, как это реализовать на программном уровне. Мы вовремя об этом заговорили:. вспомните разработку игры «Астероиды».
Вот что нам надо:
§
Мы должны уметь извлекать матрицу пикселей из загруженного РСХ-образа и сохранять ее в буфере, связанном со спрайтом;
§ Более того, хотелось бы считывать сразу несколько образов из PCX-файла и загружать их в массив, связанный с одним спрайтом. Это позволит нам оптимизировать программу по скорости выполнения.
Рисунок 5.10 показывает последовательность кадров, которые приводят в движение ковбоя. Мы воспользуемся ею позже.
После того как мы загрузим данные из PCX-файла, нам необходимо иметь возможность показывать спрайт в любой позиции на экране.
Делать это нужно осторожно, поскольку запись пикселей в видеобуфер разрушает то, что было на их месте. Поэтому, мы должны уметь сохранять ту часть изображения, которая окажется закрыта спрайтом, чтобы в дальнейшем иметь возможность восстановить первоначальный вид экрана.
Давайте на этом Месте остановимся и поговорим чуть-чуть об анимации. В играх для ПК применяется два способа обновления экрана:
§ Мы можем перерисовывать весь экран целиком, как это сделано; в игре Wolfenstein 3D;
§ Можно перерисовывать лишь участки экрана.
Какой из способов лучше выбрать, зависит от типа игры. Если мы перерисовываем весь экран, то это нужно делать по возможности быстро, поскольку 64000 пикселей - все же довольно много. Если мы перерисовываем только участки экрана, то желательно быть уверенным, что фон после прохождения спрайта не изменится. Поскольку все игры для ПК отличаются друг от друга, то для решения конкретных специфических задач всегда надо выбирать наиболее подходящую технику.
Давайте рассмотрим способ, которым мы будем пользоваться в настоящей главе - это перерисовка участков экрана. Посмотрите на рисунок 5.11, чтобы представить последовательность событий, позволяющих спрайту правильно перемещаться по экрану.
Теперь, когда мы знаем что делать, надо попробовать это реализовать. Для начала создадим структуру данных спрайта. Листинг 5.11 содержит необходимый для этого код.
Листинг 5.11. Структура спрайта с полями для анимации.
typedef struct sprite_typ
{
int x,y; // текущая позиция спрайта
int x_old, y_old; // предыдущая позиция спрайта
int width,height; // размеры спрайта
int anim_clock; // время анимации
int anim_speed; // скорость анимации
int motion_speed; // скорость движения
int motion_clock; // время
движения
char far *frames [MAX_SPRITE_FRAMES] ; // массив
указателей
//на кадры
int curr_frame; // отображаемый кадр
int num_frames; // общее число кадров
int state; // статус спрайта
char far *background; // фон
под спрайтом
}sprite, *sprite_ptr;
Структура спрайта имеет поля для сохранения позиции и размеров образа и несколько других элементов. Сейчас мы уже готовы написать функцию для работы со спрайтами.
Прежде всего мы должны извлечь битовую карту из PCX-файла и поместить ее в массив, хранящий образы спрайта. Если вы помните, я создал файл в формате PCX (CHARPLATE.PCX), в который вы можете дорисовать свои картинки и героев. Функция, извлекающая битовые карты из РСХ-образа подразумевает, что вы создали свои образы с помощью этого файла. Программа из Листинга 5.12 позволяет перемещать спрайт, который вы хотите изменить, в указанные координаты.
Листинг 5.12. Функция извлечения спрайта из загруженного PCXфайла.
void PCX_Grap_Bitmap(pcx_picture_ptr image, sprite_ptr sprite, int sprite_franie, int grab_x, int grab_y)
{
// функция выделяет одно изображение из буфера в который
// загружен PCX-файл
// функция исходит из предположения, что в действительности массив
// пикселей размером 320х200 разделен на отдельные изображения
// размером 24х24 пикселя
int x_off,y_off, х,у, index;
char far *sprite_data;
// вначале выделяем память для хранения спрайта в структуре спрайта
sprite->frames[sprite_frame] = (char far *)malloc(SPRITE_WIDTH * SPRITE_HEIGHT);
// создаем альтернативный указатель на эту область памяти
// для
ускорения доступа
sprite_data = sprite->frames[sprite_frame];
// теперь перемещаем битовый образ спрайта из области PCX-файла
// в выделенную память
// мы должны выбрать, какой именно спрайт мы копируем
// помните, что в действительности файл представляет собой массив.
// 12х8 элементов, каждый из которых имеет размер 24х24 пикселя.
// Индекс (0,0) соответствует верхнему левому углу спрайта,
// (11,7) - нижнему правому
х_off = 25 * grab_x
+ 1;
у_off = 25 * grab_y + 1;
// вычисляем начальный адрес
y_off
=y_off * 320;
for (y=0; y
{
for (x=0; x
{
// получить очередной байт текущей строки и поместить его
//в следующую позицию буфера спрайта
sprite_data[у*24 + х] = image->buffer[y_off + x_off + х];
} // конец копирования строки
// перейти к следующей строке
y_off+=320;
} // конец копирования
// инкрементировать счетчик кадров
sprite->num_frames++;
} // конец функции
Эта функция по указателю на спрайт определяет его расположение в загруженном файле. Далее она выделяет память для хранения образа и инициализирует структуру данных (я решил делать спрайты размером 24 на 24 пикселя, но вам ничто не мешает изготавливать любые другие спрайты). Теперь, когда у нас подготовлены образы, следующим шагом будет их отображение на экране монитора. Для этого нам надо:
§ Вычислить начальную позицию спрайта согласно его координатам (х,у);
§ Преобразовать полученные координаты в адрес видеобуфера;
§ Переместить байты изображения в видеобуфер.
Для рисования спрайта мы должны выполнить все операции с текущим кадром анимации. Код в Листинге 5.13 делает все перечисленное.
Листинг 5.13. Функция рисования спрайта.
void DrawSprite (sprite_ptr sprite)
{
// функция, рисующая спрайт на экране строка за строкой,
// очень быстро. Вместо умножения используется сдвиг
char far *work_sprite;
int work_offset=0,offset,x,у;
unsigned char data;
// создаем альтернативный указатель на спрайт для ускорения доступа
work_sprite = sprite->frames[sprite->curr frame];
// вычислить смещение спрайта в видеобуфере
offset = (sprite->y << 8) + (sprite->y << 6) + sprite->x;
for (y=0; y
{
for (x=0; x
{
// Проверка на "прозрачный" пиксель (с кодом 0).
Если пиксель
// "непрозрачный" - выводим его на экран.
if ((data=work_sprite[work_offset+x])) video_buffer[offset+xj = data;
} // конец вывода строки
// перейти к следующему ряду пикселей в видеобуфере
//в буфере
спрайта
offset += SCREEN_WIDTH;
work_offset += SPRITE_WIDTH;
} // коней вывода спрайта
} // конец функции
Эта функция работает примерно так же, как и Plot_Pixel_Fast из Листинга 5.5. Сначала вычисляется стартовый адрес расположения спрайта, а затем все его байты строка за строкой переписываются в видеобуфер.
Следует учесть, что здесь потребуется некоторая оптимизация. Почему бы не использовать функцию memcpy, чтобы копировать всю строку разом (а всего 24 строки)? Однако этого сделать нельзя, так как при выводе спрайта нам необходимо применить технику, использующую "прозрачные" пиксели. Что это дает? Взгляните на рисунок 5.12.
Спрайт слева выведен вместе с фоном, а при рисовании правого спрайта использовалась техника «прозрачных» пикселей. При этом пиксели с цветом фона (черный, имеющий код 0) пропускались, а прорисовывались только данные непосредственного изображения. Это и создает эффект «прозрачности» фона.
Следующая функция, которую я собираюсь вам предложить, будет сохранять фон перед тем, как мы выведем спрайт на экран. Помните, когда мы что-то записываем в видеобуфер, данные или образ, находящийся там, обязательно теряются. Поэтому мы и должны сохранять фон под спрайтом прежде чем поместим его в видеобуфер, чтобы позже восстановить прежний вид экрана. Код в Листинге 5.14 именно это и делает.
Листинг 5.14. Сохранение фона под спрайтом.
void Behind_Sprite(sprite_ptr sprite)
{ // функция сохраняет область видеопамяти, в которую будет
// выводиться
спрайт
char far *work_back;
in work_offset=0,offset,y;
// создаем альтернативный указатель для ускорения доступа
work_back = sprite->background;
// вычисляем смещение в видеобуфере
offset = (sprite->y << 8) + (sprite->y << 6) + sprite->x;
for (y=0; y
{
// копируем строку видеопамяти в буфер
_fmemcpy((char far *)&work_back[work_offset], (char far *)&video_buffer[offset], SPRITE_WIDTH);
// переходим к следующей строке
offset += SCREEN_WIDTH;
work_offset += SPRITE_WIDTH;
} // конец цикла for
} // конец функции
Функция Behind_Sprite считывает матрицу размером 24х24, где должен находится спрайт. Данные после считывания находятся в поле background структуры спрайта. Это поле является указателем на область памяти, где находится спрайт.
Собственно, это все, что нам нужно для создания и перемещения маленьких образов по экрану. Для анимации мы должны изменять поле curr_frame в структуре спрайта, перед тем, как его начать рисовать. Мы обсудим процесс анимации в этой главе, но, я думаю, вы и сами догадываетесь, как это сделать: надо стереть спрайт, передвинуть его, снова нарисовать и т. д. Вот и все.
Средства связи видеоигр
К этому моменту вы уже должны быть готовы писать видеоигры. Вы от начала и до конца научились тому, что для этого необходимо знать. Однако мы до сих пор упускали один важный момент: как написать игру, чтобы в нее могли играть сразу несколько человек? Это нам покуда совершенно неизвестно.
Существуют видеоигры, поддерживающие модемное соединение, но большинство людей или не пользуются этой возможностью, или чувствуют, что она слишком сложна, чтобы к ней прибегать. В любом случае, вопрос в этой области все еще остается открытым и вам предстоит проделать немало работы, если действительно займетесь программированием игр.
Поскольку нас первоначально интересует написание видеоигр для потребителей, мы будем концентрировать наши усилия на использовании серийных портов как средства коммуникации (а не сетевые коммуникации типа IPX/SPX или NETBIOS). Я выбираю параллельный порт в отличие от использования нескольких Ethernet плат по следующим причинам:
§ Во-первых, у каждого компьютера есть свой параллельный порт;
§ Во-вторых, у многих людей есть свои модемы, с помощью которых они обладают основным средством для игры вдвоем.
Если мы собираемся писать видеоигры для двух или более игроков, то должны рассчитывать на это с самого начала. Вы не можете написать сложную игру и только потом вдруг решить, что делаете ее как игру для нескольких игроков. Необходимо помнить, что разработка двух независимых игр, которые запускаются и синхронно работают на двух различных ПК, требует приличных трудовых затрат и предварительного планирования.
Статус клавиш
Мы должны иметь возможность определять:
§ Была ли нажата какая-нибудь клавиша;
§ Какая была нажата клавиша;
§ Статус клавиши Shift.
Статус клавиш — это просто битовый вектор (последовательность), содержащий информацию о клавишах Shift, Alt, Ctrl и других. Эта последовательность находится в памяти по адресам 417h и 418h. Мы не будем читать эти ячейки напрямую, а воспользуемся BIOS и Си.
Листинг 3.4 содержит код, позволяющий получить статус клавиш.
Листинг 3.4. Получение статуса клавиш.
#define SHIFT_R 0х0001
#define SHIFT_L 0х0002
#define CTRL 0х0004
#define ALT 0х0008
#define SCROLL_LOCK_ON 0х0010
#define NUM_LOCK_ON 0х0020
#define CAPS_LOCK_ON 0х0040
#define INSERT_MODE 0х0080
#define CTRL_L 0х0100
#define ALT_L 0х0200
#define CTRL_R 0х0400
#define ALT_R 0х0800
#define SCROLL_LOCK_DWN 0х1000
#define NUM_LOCK_DWN 0х2000
#define CAPS_LOCK_DWN 0х4000
#define SYS_REQ_DWN 0х8000
unsigned int Get_Control_Keys(unsigned int mask)
{
// функция возвращает статус интересующей нас управляющей клавиши
return(mask &_bios_keybrd(_KEYBRD_SHIFTSTATUS));
} // конец функции
В листинге 3.4 функция Get_Control_Key() использует вызов BIOS из Си для определения статуса клавиш. В строки #define включены описания масок для определения статусных клавиш, благодаря чему вы можете вызывать функцию Get_Control_Key(), не задумываясь о значении битов состояния. Более того, используя маски и логический оператор AND, за один вызов можно получить сразу несколько состояний.
Стратегия игровых коммуникаций
Соединение двух ПК и запуск на них сетевой игры является комплексной задачей, не имеющей какого-то общего решения. Все зависит от конкретной цели, которую вы перед собой поставите и решение проблемы, скорее всего, будет меняться от игры к игре. Однако, существует несколько правил, которые всегда нужно принимать во внимание. Именно об этом мы и поговорим в ближайшее время.
В игре для двух участников, которая запускается на одном компьютере, оба игрока имеют равные шансы влиять на игровую ситуацию. Рисунок 14.4 показывает два различных представления этой разновидности игр.
Однако если такая игра запускается на разных машинах, взаимоотношения игроков с внутренним миром компьютеров представляется более сложным, и это отражено на рисунке 14.5.
Проблемы, возникающие при такой конфигурации, в основном связаны с отсутствием в непосредственной близости другого игрока (как-нибудь мы осветим тему дистанционного управления состоянием компьютера). Кроме того компьютер должен получить достаточно информации, чтобы он смог показать действия игрока за другим компьютером.
Для преодоления этих проблем существуют следующие пути:
§
Можно передать на другой компьютер полную информацию о действиях игрока. В этом случае коммуникационный порт будет напоминать виртуальное устройство ввода данных, управляемое другим компьютером. Когда игрок тронет клавиши, переместит мышь или повернет ручку джойстика это действие тут же передается по кабелю другой машине, которая па основе полученных данных может скорректировать виртуальное местоположение игрока в собственном игровом пространстве;
§ Второй метод называется «синхронизацией вектора состояния». В этом случае вместо передачи данных от устройств ввода/вывода, мы передаем «состояние» игрового пространства в целом, так что принимающий компьютер может синхронизироваться с передающим, как показано на рисунке 14.6. Этот метод работает достаточно хорошо, однако при его использовании может значительно увеличиться количество передаваемой информации.
Вскоре мы разберемся с каждым из методов более, детально, а сейчас стоит поговорить о наиболее типичных ошибках, встречающихся при соединении двух ПК:
§ Наибольшая проблема возникает, когда две машины теряют синхронизацию. Скажем, одна из них имеет 586-й процессор, а другая - 386-й. При этом один ПК неизбежно окажется впереди другого и синхронизация будет потеряна. Этот фактор должен быть принят во внимание еще на этапе разработки игры;
§ Следующая потенциальная проблема может быть вызвана так называемым «недетерминированным эффектом наложения» (я расскажу лишь о некоторых лежащих иа поверхности вещах, однако этого достаточно для понимания сути проблемы). Обе игры должны быть полностью детерминированы. Это значит, например, что мины на разных компьютерах не могут оказаться в различных местах. Если на одной машине мина расположена скажем, в центре игрового поля, то и на другой машине ей лучше бы оказаться в том же месте. Точно так же, при использовании генератора случайных чисел для управления поведением существ, необходимо, чтобы на обеих машинах генерировалась одна и та же последовательность случайных величин. Единственным путем преодоления этой проблемы может служить передача полной информации об игровой ситуации, так чтобы даже случайные события, происходящие на одной машине, без искажений отражались на другой,
Эти проблемы действительно очень серьезны и вам необходимо их тщательно проработать. Мы кратко обсудили основные методы синхронизации и теперь уже можно поговорить о них более подробно.
Структуры данных, используемые для представления игрового пространства
Если бы мы решили воссоздать некий мир во всех его подробностях, то эта глава заняла бы, наверное, миллион страниц. Поэтому в играх мы имеем дело с приближенной к действительности реальностью. Зачем нам создавать весь мир в нашей игре? Нам достаточно смоделировать только то, что имеет непосредственное отношение к сюжету.
Поскольку в качестве прообраза нашей игры мы выбрали Wolfenstein и DOOM, давайте поговорим о том, как создавался мир этих игр. А затем используем эту концепцию как фундамент для нашей собственной компьютерной игры. Но прежде чем сделать это, вкратце обсудим «стандартные» способы представления трехмерного мира на основе многоугольников.
В качестве примера возьмем гипотетические трехмерные скачки. В такой игре нам понадобятся деревья, дома, беговая дорожка, трибуны и пункт контроля. При стандартном способе представления пространства, состоящего из многоугольников, эта информация должна быть представлена на уровне объектов, то есть для каждого трехмерного объекта нам потребуется его модель. Эти модели будут представлены в виде списка многоугольников, заданных их вершинами. Затем каждому объекту надо будет сопоставить координаты, ориентацию и масштаб. Следовательно, если мы хотим, чтобы у нас было десять разных зданий, мы должны определить десять разных объектов: по одному на каждый тип зданий. Затем мы можем взять каждый объект и создать связанный список.
Завершив список, мы получим возможность строить (или визуализировать) каждый объект. Подобные же списки следует создать для структур данных деревьев, трибун и т. д.
Наконец, непрерывная беговая дорожка будет представлена списком прилегающих многоугольников. Таким образом, весь мир игры будет представлен в виде списков объектов. На рисунке 11.1 показана схема такого обьектно-прогтуанственного представления игрового пространства.
Это неплохой и вполне рабочий способ представления мира. Однако, представляя игровое пространство подобным образом, достаточно сложно определять столкновения объектов, создавать новые уровни и хранить данные.
В компьютерных играх желательно иметь возможность быстро создавать новые Уровни на основе простых элементов. Поэтому стоит взглянуть на структуры Данных и методы их представления с точки зрения простоты создания нового варианта игрового пространства. Используя такой подход, мы можем проиграть в гибкости, но это окупится долгой жизнью игры.
Возьмем тот же пример скачек, реализуем его в виде двухмерного пространства, а затем придадим ему видимость трехмерности. В четырнадцатой главе, «Связь», мы подробно рассмотрим создание законченной компьютерной игры с «клеточным» пространством. Сейчас же просто попробуем разобраться в том, что собой представляет этот метод.
В большинстве компьютерных игр различные объекты и изображения многократно повторяются в разных комбинациях. Если вы внимательно посмотрите на игру РасМап, то заметите, что, несмотря на обилие лабиринтов, каждый из них состоит из одних и тех же объектов. Это таблетки, которые возвращают игроку силу, точки, повороты налево и направо, прямые стены и так далее, Пристальный анализ каждого экрана игры РасМап, покажет вам, что вся графика составлена из ряда «клеточек». Эти «клетки» в совокупности образуют повороты, прямые ходы и все остальное. Преимущество такого метода заключается в том, что он позволяет создать большой и разнообразный мир на основании очень небольшого количества исходных данных- Кроме того, такой метод позволяет легко определять столкновения объектов в нашем игровом пространстве.
Для того чтобы создать «клеточный» мир, мы должны сделать следующее:
§ Во-первых, решить, из какого количества клеток будет состоять наше пространство. Например, пусть мир нашей игры будет размером 10х10 клеток;
§ Затем мы должны задать размер клетки. Для растрового двухмерного изображения можно, например, выбрать 16х16 пикселей для одной клетки. Таким образом, наше игровое пространство будет выглядеть так, как это показано на рисунке 11.2.
§ Затем мы должны нарисовать варианты клеток для нашей игры. Мы можем нарисовать клетки, содержащие изображения маленького дерева, стены, еды и так далее;
§ После этого мы должны пронумеровать фрагменты. Таким образом, у каждого из них будет свой целочисленный идентификатор;
§ Затем мы должны задать структуру данных (обычно в виде двухмерной матрицы) и заполнить ее соответствующими идентификаторами.
Преставление данных для нашего примера «клеточного» игрового пространства показано на рисунке 11.3.
Таким образом, мы разбили одно сложное растровое изображение в нашей игре на набор фрагментов. Затем мы'создали игровую матрицу, которая задает растровое изображение как совокупность определенных фрагментов, используя Для их идентификации целые числа (идентификаторы фрагментов),
Теперь мы можем легко визуализировать игровое пространство: у нас есть его матрица, по которой несложно выбрать необходимые фрагменты изображения. Мы также имеем сами эти фрагменты (клетки). Фактически, мы просматриваем матрицу, клетка за клеткой, и в соответствии с идентификаторами воссоздаем необходимое растровое изображение.
На рисунке 11.4 показано построение растрового изображения «клеточного» игрового пространства.
Как вы можете видеть из рисунка 11.4:
§ Мы заполнили структуру данных игры (обычно это двухмерная матрица) Целыми числами, причем каждое число - это идентификатор того фрагмента, изображение которого мы хотим поместить в соответствующей клетке;
§ Затем во вложенном цикле определяем идентификаторы клеток пространства;
§ В соответствии с каждым идентификатором визуализируем необходимый фрагмент растрового изображения.
Несколько таких фрагментов будут составлять изображение здания, дороги, объекта игры и т. д.
Мы должны только побеспокоиться о правильном позиционировании каждого фрагмента. В нашем примере при переходе к следующей клетке мы должныпередвигаться сразу на величину размера клетки, то есть на 16 пикселей.Нарисовав весь экран (10х10 клеток), мы получим растровое изображение160х160 пикселей.
Используя клеточное построение игрового пространства, легко создавать новые уровни в игре. Более того, такой метод упрощает создание инструментов для изготовления новых игровых пространств. Несложно написать программу, которая будет рисовать на экране сетку, а затем с помощью пиктограмм или цветов представлять то растровое изображение игрового пространства, которое вам нужно. То есть вы размещаете пиктограммы или помечаете цветом нужные клетки, а программа транслирует их в соответствующие идентификаторы фрагментов и записывает результирующую матрицу. Таким образом, в рекордно короткие сроки можно создать десятки различных уровней для своей игры, причем каждый из них займет минимум оперативной памяти.
Единственный недостаток данного метода заключается в том, что мы не можем поместить объект в произвольное место. Он обязательно должен вписываться в границы клетки. Однако обычно это не является проблемой.
Подобная техника была использована в DOOM и Wolfenstem. Только вместо фрагментов, представляющих плоское растровое изображение, там используются трехмерные кубы, составленные из одной или нескольких текстур. Создавая нашу игру, мы можем поступить так же.
Если вы играли в Wolfenstein, то наверняка заметили, что мир этой игры создан с - помощью набора кубов, которые покрыты текстурой с четырех вертикальных сторон. Каждый из этих кубов в действительности является клеткой в двухмерной решетке. Идентификаторы каждого модуля задают текстуры, которые будут расположены соответственно по четырем сторонам. Программа считывает эти значения и на их основании создает трехмерный мир.
Создать трехмерный мир с помощью двухмерной решетки помогает её регулярная структура и одинаковая высота всех кубов. Таким образом, подобное игровое пространство получается как бы в результате экструзии плоского клеточного мира.
Клеточное пространство удовлетворяет практически всем требованиям реальной игры. Однако иногда вам могут потребоваться некоторые дополнительные возможности, которые мы уже разбирали при обсуждении объектно-пространственного метода (метода многоугольников). С помощью клеточного подхода удобно создавать окружающую среду игры. Однако иногда ограничения этого метода делают невозможным создание определенных типов объектов, и тогда целесообразно использовать другую структуру данных. Например, иная структура просто необходима, если по ходу игры требуется размещать мелкие объекты в произвольных местах.
В трехмерном мире DOOM также имеются двухмерные стационарные объекты, такие как еда, оружие и т. д. Я подозреваю, что эти объекты имеют другую структуру данных, возможно связанную с конкретным помещением. Эта дополнительная структура данных задает определенное положение и тип объекта, который может быть расположен в данной комнате.
В любом случае, используйте те методы, которые позволяют реализовать ваши замыслы. Однако не забывайте о том, что новые уровни и новые объекты Должны создаваться как можно более простым путем. Вы не должны каждый Уровень строить с нуля. Вы должны иметь для этого инструменты и максимально использовать уже готовые элементы.
Теперь, создав игровое пространство, посмотрим как его объекты взаимодействуют между собой.
Структуры данных в компьютерных играх
Теперь я хотел бы обсудить, какие структуры данных для представления объектов используются в компьютерных играх. Объектами игры может быть оружие, фантастические создания, различные предметы, которые нужно искать ц т. д. Создавая объект для игры, вы должны продумать, какими свойствами он должен обладать и отразить это в структуре данных.
Для примера рассмотрим, что требуется иметь для статического объекта. Под статическими я понимаю объекты, которые игрок может подобрать по ходу игры (в то время как динамическими я буду называть те объекты, которые могут перемещаться самостоятельно). Статический объект не совершает никаких действий, он просто лежит в определенном месте игрового пространства. Когда ваш герой находит такой объект, он поправляет свое здоровье, подкрепляет упавшие силы, пополняет арсенал и т. д. Листинг 11.1 содержит возможную структуру данных такого объекта.
Листинг 11.1. Структура данных статического объекта.
typedef struct static typ
{
int x,y; //позиция объекта
int type; // тип объекта: еда, энергия или здоровье
char *data; // указатель на растровое изображение объекта
int state; // состояние объекта
int amount; // поле для уточнения типа объекта
} static, *static ptr;
Для начала вполне можно использовать Листинг 11.1, а потом дополнять его необходимыми полями. Мы могли бы создать связанный список с использованием указателей, однако если объектов в игре немного, то лучше использовать массив, описав его следующим образом:
static stuff
[10]
Осталось инициализировать массив stuff, а затем, работая со статическими объектами, ссылаться на него.
Другой пример. Давайте рассмотрим, чем обладает в игре сам игрок. В нашей демонстрационной игре Warlock игрок - это волшебник, он имеет заклинания, здоровье, несколько жизней, а также может собирать различные предметы. Кроме этого, нам понадобятся различные растровые изображения его рук, видимых на переднем плане экрана. Листинг 11.2 показывает первое приближение обходимой структуры данных.
Листинг 11.2.
Структура данных игрока.
typedef struct player_typ
{
int x,y; // Позиция игрока
int lifes; // Количество оставшихся жизней
int health; // Уровень здоровья
int weapons[3]; // Массив, содержащий типы оружия, которые
// есть у игрока
int spells [10] ; // Массив, содержащий заклинания, которые
// имеются у игрока
char *hands_stationary; // Растровое изображение рук игрока, когда
// он ничего не делает
char *hand_motion[4]; // Четыре растровых изображения рук игрока
// для выполнения заклинаний
int state; // Состояние игрока: жив, мертв, умирает
} player, *player_ptr;
(Мы коротко расскажем о состояниях объектов чуть позже в этой главе, а более подробно —в тринадцатой главе, "Искусственный интеллект").
Как вы можете видеть, мы определили большинство необходимых переменных. Кое-что мы, конечно, добавим или выкинем позже, но для начала эта структура вполне подойдет. Это простой пример того, как следует создавать объекты в своей игре. Исходя из того, какая информация вам понадобится, организуйте соответствующую структуру и двигайтесь дальше.
Если в вашей игре есть как статические, так и динамические объекты, вы должны написать код, который будет рисовать, передвигать, отрабатывать столкновения и уничтожать объекты. Однако вам наверняка не захочется писать новые функции для каждого отдельного дерева! Давайте посмотрим, как можно клонировать игровые объекты.
Сверхскоростная очистка экрана
Экран в режиме 13h отображается в области памяти, начиная с адреса А000:0000 и по A000:FBFF. При этом каждый пиксель задается одним байтом. Давайте посмотрим на рисунок 2.3, чтобы лучше понять, как это происходит. В данной конфигурации каждый пиксель может принимать одно из 256 значений, но эти значения не являются, как можно было бы подумать, кодом цвета пикселя. Они представляют собой видимое значение цвета.
Теперь поговорим о том, как это работает. Значение пикселя, а это байт, адрес которого определяет положение пикселя на экране, используется в качестве индекса в гигантской таблице цветов. Таким образом, значение пикселя 26 не означает цвет номер 26. Наоборот, это значит «индекс, указывающий на 26-й элемент таблицы и использующий значение этого поля».
Примечание
Таблица цветов содержит по одному байту для каждого из первичных цветов, Однако реально используются только шесть первых бит каждого байта. Таким образом, каждый из элементов таблицы состоит из трех байтов, определяющих значения трех основных цветов; красного (R - Red), зеленого (G - Green) и голубого (В — Blue), которые в сумме позволяют представить 262114 цветов. Однако размер таблицы ограничен 256-ю элементами, поэтому и на экране может одновременно присутствовать не более 256 цветов.
В видеоиграх, которые нас привлекают, экран перерисовывается от 15 до 30 раз в секунду. Таким образом, перед тем как нарисовать что-то новое на экране, нам необходимо удалить старое изображение. Для того чтобы это делать, надо найти способ быстрого заполнения видеобуфера каким-нибудь значением цвета, например, цветом фона.
Это значение будет заполнять всю видеопамять, а, следовательно, и весь экран в виде цвета, Самый быстрый способ сделать это - воспользоваться ассемблерной инструкцией STOSW. Вы можете спросить: «А зачем использовать STOSW, когда каждый пиксель — это байт, в то время как STOWS оперирует со словами (WORD)?". На этот вопрос можно дать два ответа:
§ Во-первых, коль мы можем записать один байт в видеопамять, то значит, можем записать и два;
§ Во-вторых, нам надо минимизировать количество обращений к видеопамяти, поскольку она работает примерно в 10 раз медленнее, чем обычная память. Поэтому, предпочтительнее писать не по одному байту, а сразу по два.
Листинг 2.10 показывает ассемблерную функцию для заполнения экрана определенным цветом, а Листинг 2.11 содержит программу на Си, тестирующую ее.
Листинг 2.10. Процедура, заполняющая экран (FILLA.ASM).
screen_ram EQU 0A000h ; видеопамять в этом режиме начинается
; по адресу A000:0000h
.MODEL MEDIUM, С ;устанавливаем модель памяти MEDIUM,
; соглашения по вызову языка Си
.CODE ; начало кодового сегмента
PUBLIC Fill_Screen ; объявляем процедуру общедоступной,
Fill_Screen PROC FAR С color : WORD ;функция принимает один параметр
mov AX, screen_ram ;ES:DI должно указывать на видеопамять
mov ES,AX
xor di,di ;обнуляем DI
mov CX,320*200/2
;количество слов, которое надо вывести
mov AL,BYTE PTR color ;помещаем в регистр AL код цвета
mov AH,AL ; этот же код помещаем в регистр АН
rep STOSW ;эта команда заполняет видеопамять
; выбранным цветом с максимально
; возможной скоростью
RET ;выход из процедуры
Fill_Screen ENDP ;конец процедуры
END ;конец кодового сегмента
Листинг 2.11, Программа на Си для тестирования программы 2.10 (FILLC.C).
#inciude
#define VGA256 0х13
#define TEXT_MODE 0х03
extern Set_Mode(int mode);
extern Fill_Screen(int color);
void main(void)
{
int i;
// устанавливаем режим 320х200 точек, 256 цветов (режим 13h)
Set_Mode(VGA256);
// заполняем экран цветом с кодом 1 (в,палитре, устанавливаемой
// по умолчанию, это соответствует синему цвету)
for (t=0; t<1000; t++) Fill_Screen(1) ;
// ждем нажатия любой клавиши
while(!kbhit()) {}
// возвращаемся в текстовый режим работы экрана
Set_Mode(TEXT_MODE);
} // конец функции main
Эти программы чистят экран с максимальной скоростью.
Примечание
Я произвел замер скорости работы этих функций на своем компьютере и получил значение 22 кадра в секунду. Это представляется невероятно медленным, и я сильно забеспокоился. Однако при ближайшем рассмотрении выяснилось, что причина задержки — крайне низкое быстродействие видеопамяти. Собственно, процессор мог бы обеспечить скорость до 250 кадров в секунду. Однако, увы, он часто вынужден ждать, пока видеопамять соизволит откликнуться на его обращение.
На прилагаемой к этой книге дискете вы найдете программу под названием GAUGE.EXE, Вы можете использовать ее для замера производительности вашей видеосистемы.
Наш курс ассемблера проходит отлично. Я уже сам узнал кучу нового и надеюсь, что вы тоже. Теперь нам осталось узнать еще про одну возможность программирования на ассемблере: об использовании встроенного (in-line) ассемблера,
Связь мультипликации с контекстом
Как это ни удивительно, но на связь мультипликации с контекстом крайне редко обращают внимание в компьютерных играх. Контекст означает «связь с окружением или основой». В играх контекстом являются разнообразные дейст вия по ходу игры. Что бы ни делал персонаж в игре: умирал, прыгал, стрелял или еще что-нибудь, -- все это часть контекста игры. Мультипликация и различные эффекты, которые связаны с контекстом, придают игре дополнительную реалистичность и размах.
Попробую пояснить на примере, что я имею в виду. Представьте, что мы имеем игру, в которой герой может гулять, прыгать, бегать и стрелять. Независимо от того, перепрыгивает ли он через паука или озеро, во время прыжка мультипликация обычно бывает всегда одна и та же. Но что если для каждого случая, или соответствующего контекста мы сделаем отдельный мультик? Например, когда герой перепрыгивает просто через лужу, то он не совершает ничего кроме собственно прыжка. А вот когда ему приходится перепрыгивать через паука, то он может, например, издать пронзительный крик ужаса и показать изумительную сноровку в прыжках. (И действительно, приземление на большого склизкого паука, возможно, не самый лучший опыт для маленького героя, которого заманили в виртуальный компьютерный мир.)
Итак, контекстнозависимая мультипликация, используя набор соответствующих вариантов движений, учитывает тем самым окружение и внешние обстоятельства в каждый момент игрового действия, что дополнительно разнообразит зрительное восприятие игры.
Связь
Так же, как единственная клетка мозга не могла бы сделать больших успехов в шахматах, играть с компьютером в одиночестве не очень интересно. В связи с последними достижениями в области телекоммуникаций, видеоигры для нескольких игроков становятся все более распространенными. В настоящее время многие программы поддерживают игру двух человек через модем. В этой главе мы начнем изучать системы связи между ПК и приемы, необходимые для создания видеоигр, рассчитанных на нескольких игроков.
В этой главе будут изучены следующие темы:
§
Средства связи видеоигр;
§ Последовательный интерфейс ПК;
§ Функции поддержки последовательного порта ROM BIOS;
§ Соединение через нуль-модем;
§ Создание коммуникационных библиотек;
§ Стратегия коммуникационных видеоигр;
§ Синхронизация вектора состояния;
§ Синхронизация состояния порта ввода/вывода;
§ Синхронизация по времени;
§ Модем;
§ Написание игры Net-Tank (Сетевой танк) для двух игроков в замкнутом пространстве.
В этой главе мы сконцентрируем внимание на проблемах дизайна игр для нескольких игроков, по возможности не вдаваясь в управление коммуникациями как таковыми. К сожалению, у нас нет времени на изучение методов осуществления связи через модем в полном объеме. Однако мы научимся управлять связью через последовательный порт с помощью нуль-модема. С этими знаниями, потратив дополнительно несколько ночей, вы научитесь связываться через модем. У вас будут все элементы, достаточные для того, чтобы получить нечто работающее. Мы хотим научиться писать видеоигры для двух и более игроков сидящих за своими компьютерами и играющими одновременно без потери синхронизации и прочих проблем. Поэтому нас больше будет интересовать тактика коммуникации в архитектуре видеоигр, а не физическая коммуникация сама по себе.
Таблицы цветов
Как я уже говорил, мы можем записать по рассчитанному адресу число от 0 до 255. А что означает это число? Ответ прост - это цвет пикселя, который мы хотим отобразить.
VGA-карта способна одновременно отобразить на экране до 256 цветов. Цвет, который мы хотим получить, должен быть представлен числом от 0 до 255. Это здорово, но какая связь между числом и действительным цветом? Число используется как индекс в таблице цветов, хранящей действительные значения цвета, который мы увидим на экране.
Всего же VGA-карта способна отобразить 262144 цвета. Таким образом, если нам надо записать в видеобуфер значение цвета, то нам понадобится три байта для представления числа такой длины. Впрочем, и видеобуфер в этом случае будет просто огромен. Поэтому, создатели карты предусмотрели возможность переадресации графического адаптера.
Переадресация означает, что одна числовая величина используется в качестве адреса другого значения (примерно, как именованный указатель в Си). Вместо одновременного воспроизведения всех 262144 цветов, разработчики VGA-карт дали возможность использовать подмножество из 256 цветов. В результате VGA-карта имеет таблицу отображения цветов, включающую 256 значений. Каждое из этих значений состоит из 256 элементов размером в один байт, содержащих значения красного, синего и зеленого для выбранного цвета (помните, красный, зеленый и синий в комбинации могут образовывать любой цвет.)
Таблица цветов состоит из 768 байт (3х256). Например, когда карта считывает из видеобуфера число 72, то она проверяет адрес 72 в таблице цветов. Адрес 72 находится по смещению 72х3 от начала таблицы, поскольку каждое из значений занимает три байта. Затем значения зеленого, красного и синего считываются из таблицы и используются как значения сигналов. Давайте рассмотрим рисунок 5.2 для более подробного ознакомления.
Было бы прекрасно, если б мы имели прямой доступ к таблице соответствия, как к обычной памяти. Но, к сожалению, регистры цвета доступны только через порты ввода-вывода VGA-карты. На самом деле это плохо, поскольку весьма усложняет нам жизнь. Теперь нам предстоит узнать, как менять значения в таблице цветов.
Тайминг
Я попытался придумать хорошую демонстрацию перемещения спрайтов и решил, что для этого подойдет маленький городок с ковбоем, который ходит по улице. Не так уж плохо. Я хотел еще, чтобы он время от времени стрелял, но позже решил не усложнять дело. Для осуществления реалистичной анимации мы должны уделять большое внимание таймингу, то есть задержке между выводом кадров. Если образ имеет 10 анимационных кадров и мы будем их менять слишком быстро, то персонаж станет похож на лунатика. Поэтому мы должны иметь в программе счетчики времени, чтобы наши персонажи выполняли определенные действия с заданной скоростью. В нашем случае мы используем четыре переменные для сохранения счетчиков движения и анимации:
§
anim_clock
§ anim_speed
§ motion_clock
§ motion_speed
Переменные скорости - это константы, а переменные времени обновляются при каждом проходе через главный цикл. Когда переменная времени оказывается больше переменной скорости, то мы осуществляем какое-либо действие: перемещаем спрайт или сменяем кадр. Переменные времени дри этом обнуляются. Это позволяет нам сделать перемещение спрайта по экрану и смену фаз движения независимым друг от друга и от быстродействия машины. Мы еще вернемся к этому в седьмой главе.
Технические приемы анимации
Внимательно изучив рисунок 16.31, вы можете заметить, что определенные части каждого кадра остаются относительно неподвижными. Положение головы и туловища человека не изменяются от кадра к кадру. Чтобы оживить персонаж было несколько легче, надо предпринять следующие действия:
1. В первую очередь нарисуйте области, остающиеся неизменными,
2. Затем сделайте необходимое количество копий кадра.
3. Наконец, дополните каждый кадр недостающими движущимися частями.
Этот прием дает вам одно преимущество: он помогает правильно расположить изображения фаз движения одно за другим. Мультипликация базируется иа неподвижных частях изображения и, выводя полученные таким способом кадры, вы можете получить достаточно плавное движение. Помните, что сдвиг неподвижной части изображения даже на один пиксель может привести к cитyaции, когда движения вашего персонажа будут выглядеть дерганными.
ТЕХНИКА ОПТИМИЗАЦИИ
В прошлом году я просил у Санта Клауса подарить мне суперкомпьютер Cray XPM, но, как обычно, он не принес ничего. И если вы не будете использовать компьютер с фемисекундным циклом выполнения команд и террабайтами оперативной памяти, нам обоим придется примириться с ПК.
Следовательно, мы должны делать наши программы видеоигр настолько быстрыми, настолько возможно. Для этого мы должны постараться выжать из ПК каждую унцию его мощности. В данной главе мы охватим следующие темы, связанные с приемами оптимизации:
§ Передача параметров;
§ Глобальные переменные;
§ Указатель и ценность псевдоимени;
§ Использование регистров;
§ Оптимизация компилятора;
§ Развертка циклов;
§ Бинарное умножение;
§ Таблицы поиска;
§ Математика с фиксированной точкой;
§ Встроенный ассемблер;
§ Предмет изучения;
§ Оптимизация построения пикселя;
§ Оптимизация изображения пикселя.
Введение
Прежде чем углубиться в изучение приемов оптимизации видеоигр для ПК, я хочу дать вам несколько советов. Когда вы оптимизируете свои программы, не пытайтесь разом провести полную оптимизацию. В первую очередь обратите внимание на те части, время выполнения которых критично и где встречается наибольшее количество циклов. Возьмите эти функции и работайте с ними до тех пор, пока их быстродействие не начнет вас удовлетворять.
Попытка оптимизации всей игры за раз, как правило, приводит к полному беспорядку, и вы никогда не сможете ни отладить программу, ни добавить что-либо в игру. Оптимизация должна быть взвешенной с точки зрения наличия других факторов. Если оптимизация функции принесет ей лишние три процента быстродействия, но при этом сделает ее вдвое запутаннее, то стоит попробовать найти другое место для оптимизации.
Не пытайтесь свалить все операторы, в кучу и свести десяток строк программы к одной. Ненавижу смотреть на выражения типа
* (х+(у++) >=*(& (х++)>>2)+(char far *)y;
Получили представление? Такая запись не только выглядит коряво с точки зрения Си, но и Codeview вряд ли поможет вам в отладке строк, подобных этой.
Поскольку мы разрабатываем видеоигры, я могу гарантировать, что 90% времени будет затрачено не на осуществление игровой логики, а на различные графические преобразования. Поэтому нужно сделать все функции рисования настолько быстрыми, насколько это возможно, а строки, управляющие игрой, имеет смысл оставить простыми и понятными. Функция, рисующая на экране точку, может представлять собой «черный ящик», а игровая программа в целом — нет. Оставляйте ее удобочитаемой. С помощью приемов оптимизации, которые мы изучим в этой главе, быстродействие практически любой функции может быть увеличено от 2 до 10 раз. Не говоря о том, что вы получите замечательные результаты, еще и произведете впечатление на друзей.
Передача параметров
Из второй главы «Основы языка ассемблерам (да и из собственного опыта) вы должны помнить, что передача параметров функциям не является свободной в полном смысле слова. Параметры должны быть помещены в стек, сделаны доступными через использование указателя базы и, наконец, взяты со стека. Если вам нужна функция, которая складывает 8 чисел, передаваемых как параметры, то потребовалось бы написать что-нибудь вроде этого:
int Add_Em_All(int nl,int n2,int n3,int n4,
int n5,int n6,int n7,int n8)
{
return (n1+n2+n3+n4+n5+n6+n7+n8);
}
(Конечно, это не является реальной функцией. Я привел ее только в качестве наглядного примера.) После компиляции этой функции ее тело будет выглядеть примерно так:
clc
mov ax,00h
adc ax,n1
adc ax,n2
adc ax,n3
adc ax,n4
adc ax,n5
adc ax,n6
adc ax,n7
adc ax,n8
Конечно, в самом начале приведенного фрагмента необходимо создать фрейм стека и уничтожить его в конце. Однако суть в том, что на восемь выталкиваний и извлечений параметров уходит немало времени. Если вы посмотрите на скорость выполнения команд PUSH и POP, то обнаружите, чти они отнимают в три раза больше тактов процессора, чем ADC. Как видите, в данном случае передача параметров отнимает больше времени, чем выполнение самой функции. Это показывает нам, что мы должны стараться передавать только те переменные, которые действительно необходимы.
Также никогда не передавайте структуры как значения. Если вы определите структуру, написав что-либо похожее на это:
typedef struct point_typ
{
int x[10],y[10],z[10];
}point,point_ptr;
сделаете вызов
Display(point point_1);
то в стек будет помещена целая структура! Это очень плохо. Чтобы избежать подобного, при передаче структур всегда применяйте ссылки на них, а «передачу значением» используйте только для целых и прочих стандартных типов данных Си.
Может быть, вы спросите: «Почему не использовать глобальные переменные вместо параметров?» Рассмотрим эту идею более внимательно.
Глобальные переменные
Мы пишем видеоигры, правила которых иногда должны изменяться. Всякий, кто где-нибудь учился, знает, что глобальных переменных стоит по возможности избегать. Конечно, всякое бывает, но лучше, когда их очень мало, и прекрасно, когда они вообще отсутствуют. Здесь приведена точка зрения одного крутого знатока в программировании игр; «Используйте глобальные переменные всегда, когда они помогают в увеличении быстродействия, но при этом сохраняйте чувство меры». К примеру, у нас есть набор функций, которые рисуют точки, изображают линии и окружности.
Эти функции требуют передачи им различного количества параметров: для построения точки нужно знать две координаты, а для рисования линии — целых четыре. Что же касается цвета, то он, вероятно может быть одним и тем же как для линий, так и для окружностей. Почему же не сделать, чтобы каждая функция рисовала текущим глобальным цветом? С этой целью можно задать переменную и назвать ее, например, draw_color. Если вы измените текущий цвет и сделаете сотню вызовов функции, то при этом изменить цвет достаточно будет только один раз. В результате вы сумеете избежать порядка двухсот операций обмена со стеком.
Но учтите, что применение глобальных переменных может оказаться немного похожим на употребление наркотика: чем больше его принимаешь, тем больше он нужен. Хороший программист всегда может сбалансировать использование глобальных переменных и увеличить с их помощью быстродействие программы на 5, а то и 10 процентов.
Указатели и использование псевдоимен
Эту тему условно можно назвать тактическим приемом, которым пользуются некоторые программисты, в то время как многие о нем даже и не подозревают. Например, у вас есть фрагмент такой программы:
for(t == y->stars[index].left;
t < stars[index].left + 100; t++)
{
position = point->x + point->y -point->z;
pitch = point->x * point->y * point->z;
roll-= point->ang + sin(t) * point->ang;
}
Этот фрагмент хоть и выглядит достаточно компактным, но, тем не менее, и к нему может быть применен способ оптимизации, связанный с использованием псевдоимен. Вы видите, что в этом примере присутствует несколько команд, ссылающихся на указатель. Тактика, которой мы здесь воспользуемся, заключается в замене всех указателей, встречающихся более двух раз, простыми переменными. Вышеуказанная функция в результате может быть переписана так;
t1=y->stars[index].left;
x=point->x;
y=point->y;
z=point->z;
ang=point->ang;
for(t=t1;t
{
position=x+y+z;
pitch=x*y*z;
roll=ang+sin(t)*ang;
}
Несмотря на то, что новая версия длиннее, она выполняется быстрее, так как содержит только пять ссылок вместо 800. Конечно, доступ к переменным х, у и z отнимает некоторое время, но порядок его величины меньше, чем ссылки на структуры и указатели. И этим нужно воспользоваться.
Использование регистров
В конечном счете, все программы, которые мы пишем на Си, будут переведены в последовательность машинных команд. После этого превращения некоторые из регистров общего назначения окажутся занятыми выполнением задачи, определяемой программой. Однако, мы не можем быть уверенными, что регистры будут использоваться вместо медленной стековой памяти. Чтобы быть уверенным, что функции используют регистры для переменной индекса (или какой-нибудь другой), давайте попробуем заставить компилятор по мере возможности делать это. Необходимо всегда помнить, что доступ к регистрам процессора во много раз быстрее, чем к оперативной памяти. Это происходит потому, что регистры находятся внутри ЦПУ, а память - нет. Мы можем использовать ключевое слово register, чтобы скомандовать компилятору использовать регистр. Как пример, напишем функцию, которая делает перестановку без регистров.
void Swap(int..num l,num 2)
{
int temp;
temp=num_1;
num_l=num_2; num_2=temp;
}
А теперь перепишем ее, используя регистр как временную переменную.
Swap(int num_1,num_2)
{
register int temp;
temp=num_1;
num_1=num_2;
num_2=temp;
}
Если компилятор может, то он будет использовать регистр в качестве регистровой переменной и программа увеличит скорость выполнения на 10-15 процентов. В связи с использованием ключевого слова register нужно учитывать два момента:
§ Компилятор не создает регистры, он использует стандартные регистры ЦПУ;
§ Иногда форсирование компилятора для использования регистров делает программу медленнее. Будьте осторожны. Обычно не стоит применять переменные типа register в маленьких функциях.
Теперь поговорим об основных приемах оптимизации компилятора.
Оптимизация компилятора
Фирма Microsoft уверяет, что ее компилятор является оптимизирующим. Это предельно правдивое высказывание. Несмотря на то, что оптимизатор иногда в состоянии сделать вашу программу медленнее или даже привнести ошибки, он проводит классическую оптимизацию (с точки зрения ученых-компьютерщиков). Однако мы, как создатели игр, не можем доверять автоматической оптимизации. Следовательно, мы никогда не будем использовать никакие опции оптимизатора (хорошо, может быть только некоторые из них). Стоит попробовать поиграть с ними только когда ваша видеоигра уже полностью готова и свободна от ошибок. Но ни в коем случае не рассчитывайте на оптимизацию во время разработки программы. Это не панацея. Единственное, что может принести реальную пользу, так это опция отключения контроля переполнения стека.
Обычно компилятор в начале любой процедуры вставляет небольшие фрагменты кода, называемые прологом, которые служат для проверки достаточности стекового пространства для размещения локальных переменных. Но если установить размер стека в несколько килобайт, то у вас никогда не будет проблем. Поэтому вы можете выключить контроль переполнения стека, что немного сократит время обращения к функции (для этого можно использовать директиву компилятора -GS).
Я считаю, что оптимизатор на самом деле может доставить хлопот больше, нежели разрешить проблем. Вскоре я специально напишу эффективную программу, чтобы доверить ее оптимизатору и посмотреть, что из этого получится. Иногда оптимизатор может без зазрения совести внести в вашу программу пару-тройку ошибок, и это является еще одной причиной, почему им не стоит пользоваться слишком активно. (Я уже задокументировал дюжину ошибок в новом компиляторе Microsoft — Visual C/C++ 1.5, так что это высказывание имеет под собой серьезное обоснование. Обычно я до сих пор использую C/C++ 7.0, находя его более надежным.)
Развертка циклов
Это очень старо. В далеком прошлом, в конце 70-х годов, когда Apple безраздельно правил миром ПК, королем среди процессоров считался процессор 6502.
Люди всегда находили интересные пути, чтобы заставить его работать быстрее. Один из трюков, который был найден, называется разверткой циклов. Это технический прием, где программист на самом деле отменяет структуру цикла вручную, разбивая саму задачу цикла. Структура цикла сама по себе имеет небольшой заголовок и мы можем использовать это в наших целях. К примеру, мы хотели бы инициализировать поле men в 10000 структур. Можно было бы поступить так:
for (index=0;index<10000;index++)
{
player[index].men=3;
}
и это будет прекрасно работать. Однако переменная index инкрементируется и сравнивается 10000 раз. Поскольку цикл повторяется 10000 раз, то это означает, что будет 10000 переходов типа NEAR. Мы можем развернуть цикл и получить небольшой выигрыш в скорости его выполнения. Вот что мы могли бы сделать:
for(index=0;index<1000;index+=10)
{
player[index].men=З;
player[index+1].men=3;
player[index+2].men=3;
player [index+3].men=3;
player[index+4].men=3;
player [index+5].men=3;
player [index+6].men=3;
player[index+7].men=3;
player [index+8].men=3;
player[index+9].men=3;
}
Теперь цикл выполняется только 1000 раз. Следовательно, index изменяется и сравнивается только 1000 раз, и при этом выполняется только 1000 переходов типа NEAR.
Этот пример написан мною еще и для того, чтобы показать вам, что развертка цикла может иметь и отрицательные стороны. Посмотрите внимательно на новую программу. Здесь к каждому индексу в каждой последующей операции присваивания добавляется смещение, и время, которое уйдет на это, может свести на нет выгоду от разворачивания циклов. Однако, чтобы исправить это, мы можем применить маленькую хитрость. Введем для индексирования структуры вторичную переменную new_index, которую будем увеличивать после каждого присваивания. Это приведет к увеличению скорости. Взгляните:
new_index=0;
for(index=0;index<1000;index+=10)
{
player[new_index++].men=3;
player[new_index++].men=3;
player[new_index++].men=3;
player[new_index++].men=3;
player[new_index++].men=3;
player[new_index++].men=3;
player[new_index++].men=3;
player[new_index++].men=3;
player[new_index++].men=3;
player[new_index++].men=3;
}
Новая программа работает быстрее, чем старая. Неплохо? Развертка циклов настолько эффективна, что у вас может возникнуть желание постоянно прибегать к этой уловке. Однако и здесь нужно знать меру. Дело в том, что машинные команды кэшируются внутри современных CPU, и слишком «массированное» разворачивание циклов может привести к проблемам переполнения кэша. Но если вы пользуетесь этим бережно (то есть ограничиваетесь тремя-восемью итерациями), то должны получить хорошие результаты.
Теперь поговорим о другом старом трюке - использовании операций сдвига для перемножения чисел.
Бинарное умножение
Впервые мы столкнулись с этим трюком в пятой главе, «Секреты VGA-карт». На ПК (да и вообще почти на любом компьютере на этой планете) система двоичных чисел используется для представления чисел в компьютере (хотя, я слышал и о «троичных» компьютерах). Поскольку разряды двоичных чисел являются степенью двух и каждое число помещается в слове как набор двоичных цифр, сдвиг слова влево или вправо смещает каждый его разряд на соседнее место. Эти операции автоматически удваивают число или делят его на два, соответственно. Взгляните на рисунок 18.1, чтобы увидеть это на примере.
Сдвигая число влево, вы умножаете его каждый раз на 2. Проделав это четыре раза, вы умножите его на 16, пять раз — на 32. Таким путем мы можем умножить число на любую степень двух, но как насчет других'чисел, таких, например, как 26? Для выполнения этого мы разобьем умножение на группы умножений по степеням двух. Число 26 может быть представлено как 16+8+2. Таким образом, если мы умножим произвольное значение Х на 16, добавим к нему X, умноженное на 8 и, наконец, добавим X, умноженное на 2, то ответ должен быть таким же, как если бы мы умножили на 26.
Взгляните:
Y=X*26=X*16+X*8+X*2=X<<4+X<<3+X<<1
Кстати, именно так поступают ученые-ракетчики, которые прекрасно знают, что сдвиг гораздо быстрее умножения, и мы также можем выиграть кучу времени, используя эту «реактивную» технику. Увеличение скорости вычислений достигается за счет того, что сдвиг — простая бинарная операция, в то время как умножение является действием гораздо более комплексным.
В связи со сказанным стоит упомянуть еще вот о чем;
§ Во-первых, деление также может быть выполнено путем сдвига числа, но только вправо. Правда, деление сдвигом выполняется не так часто, как умножение, поскольку делитель разбить на составляющие обычно бывает значительно сложнее.
§ Во-вторых, эта техника работает только с целыми числами без плавающей запятой. Значения типа FLOAT и DOUBLE хранятся в памяти в так называемом IEEE-формате, и их сдвиг приведет к переполнению разрядной .сетки.
Теперь мы можем перейти к следующей технике оптимизации — к таблицам поиска. Что ж, поищем их...
Таблицы поиска
Таблицы поиска, как следует из их названия, служат для поиска некоторых фактов. С их помощью можно искать что угодно. Суть применения таблиц поиска состоит в том, что вместо выполнения расчетов в процессе работы программы, мы предварительно выполняем все возможные вычисления, в которых может возникнуть необходимость, и сохраняем их в гигантской таблице. Далее, во время выполнения программы, мы смотрим в таблицу, используя параметр вычисления для поиска в ней конечного результата. Наиболее классическое использование справочных таблиц — предварительное вычисление трансцендентных функций, таких как синус, косинус, тангенс и т. д., поскольку они отнимают много времени для вычисления, даже с математическим сопроцессором.
Приведу пример использования справочных таблиц. Скажем, в программе нам необходимо вычислять синус и косинус угла, который может быть любым от 0° до 360° и является целым (то есть у нас не будет вычислений для углов с десятичной частью, таких как 3.3).
Следующая программа создает справочную таблицу значений синусов и косинусов:
float sin_table[360],cos_table[360] ;
for(index=0;index<360;index++)
{
sin_table[index]=sin(index*3.14159/180);
cos_table[index]=cos (index*3.14159/180) ;
}
Этот программный фрагмент создает две таблицы, содержащих по 360 заранее вычисленных значений синусов и косинусов.
Теперь посмотрим, как использовать справочные таблицы, К примеру взглянем на это выражение:
x=cos(ang*3.14159/180)*radius;
y=sin(ang*3.14159/180)*radius;
Используя наши новые справочные таблицы, мы могли бы иметь:
x=cos_table[ang]*radius;
y=sin_table[ang]*radius;
Применение справочных таблиц может значительно увеличить быстродействие ваших игр. Единственная сложность заключается в том, что они отнимают много места. К примеру, игра Wing Commander использует справочные таблицы, которые содержат предварительно вычисленные виды кораблей для всех углов поворота. DOOM использует справочные таблицы, чтобы помочь в вычислении видимого расположения всех объектов игрового пространства.
При построении справочной таблицы учитывается все, что игрок может увидеть на экране. Необходимые для графических построений данные вычисляются перед началом игры или загружаются в готовом виде с диска. Затем, во время игры справочные таблицы получают привязку к окружающему игровому пространству и если табличные данные сообщают, что некоторую часть изображения невозможно увидеть, то графическая система устранения скрытых поверхностей удалит их.
Я сторонник идеи, что полноценная видеоигра может быть сделана только с огромным количеством справочных таблиц, и лишь небольшим программным кодом, отвечающим за осуществление игровой логики, а то и вовсе без такового. Как и в электротехнике, в видеоиграх используются конечные автоматы (с которыми мы столкнулись в тринадцатой главе, «Искусственный интеллект»). Однако вместо использования алгоритмов, моделирующих поведение объекта для следования из одного состояния в другое (как мы делали в тринадцатой главе), для управления существами можно привлечь справочные таблицы.
Этим техническим приемом в вычислительной технике пользуются для конструирования КА и мы также можем делать это в наших программах. Все что нам необходимо, это таблица, которая содержит текущее состояние и следующие состояния.
Итог: справочные таблицы великолепны. Применяйте их во всех случаях компьютерной жизни пока позволит объем памяти. Кроме того, когда вы составляете справочные таблицы, попробуйте, где это только возможно, использовать симметрию для уменьшения размера таблицы. Например, синус и косинус
по сути являются одной и той же функцией с различием в 90о. Взгляните на рисунок 18.2. Мы видим, что синус и косинус выглядят почти одинаково. Фактически, они связаны следующими формулами:
sin (angle)=cos(angle-90) cos (angle)=sin(angle+90)
Поэтому мы могли бы создать единственную справочную таблицу для значений косинусов, а потом, когда нам понадобится вычислить синус угла, достаточно к исходной величине угла добавить 90е и использовать новый результат как индекс. Конечно, при этом нужно быть осторожным — новый угол может оказаться больше 360°. В этом случае вы должны выполнить его циклический возврат к нулю:
if(angle>360) angle=angle-360;
Для, доказательства того, что справочные таблицы могут увеличить скорость выполнения программ, я создал программку, которая использует как обычные функции sin и cos, так и справочную таблицу, хранящую заранее вычисленные значения синусов и косинусов, изображающую 100 окружностей. Программа, показанная в Листинге 18.1, начинается с заполнения таблиц. Затем она чертит 1000 окружностей, используя внутренние функции. Потом она ждет нажатия клавиши, после чего рисует 1000 окружностей, используя табличные данные. Запустив программу, вы обязательно почувствуете разницу. Также обратите внимание, что эта программа достаточно компактна и очень эффективна с точки зрения вывода графики.
Листинг 18.1. Сравнение выполнения программы с использованием справочных таблиц и встроенных функций sin и cos (LOOKNUP.C).
#include
#include
#include
float sin_table[360], cos_table[360];
main()
{
int index, x,y,xo,yo,radius,color,ang;
char far *screen = (char far *)0xA0000000;
// использовать библиотеку Microsoft для перехода
// в режим 320х200х256
_setvideomode(_MRES256COLOR);
// создать таблицы быстрого доступа
for (index=0; index<360; index++)
{
sin_table[index]= sin(index*3.14159/180} ;
cos_table[index]= cos(index*3.14159/180);
}
// нарисовать 1000 окружностей, используя встроенные
// функции sin и cos
for (index=0; index<1000; index++)
(
// получить случайные числа
radius = rand()%50;
xo = rand()%320;
yo = rand(}%200;
color = rand()%256;
for (ang=0; ang<3 60; ang++)
{
x = xo + cos(ang*3.14159/180) * radius;
У = yo + sin(ang*3.14159/180) * radius;
// нарисовать точку окружности
screen[(y<<6) + (y<<8) + x] = color;
}
}// все, ждать пока пользователь нажмет клавищу
printf("\nHit, a key to see circles drawn twith look up tables.");
getch();
_setvideomode(_MRES256COLOR);
// нарисовать 1000 окружностей, используя таблицы поиска
for (index=0; index<1000; index++)
{
// нарисовать случайную окружность
radius = rand()%50;
хо = randO %320;
уо = rand()%200;
color = rand()%256;
for (ang=0; ang<3 60; ang++)
{
x = хо + cos table[ang] * radius;
у = уо + sin_table[ang] * radius;
// нарисовать точку окружности
screen[(y<<6) + (y<<8) + x] = color;
} }
// подождать, пока пользователь нажмет любую клавишу
printf("\nHit any key to exit."};
getch();
_setvideomode(_DEFAULTMODE);
}
После запуска LOOKNUP.C вы должны согласиться, что справочные таблицы крайне удобны и могут здорово увеличить скорость выполнения программы.
Следующая тема будет касаться математики с фиксированной запятой.
Математика с фиксированной запятой
Математика с фиксированной запятой? Нет, это не новая точка зрения на дробные числа. Просто это немного другой путь рассмотрения компьютерной математики.
Существуют две формы математики, предназначенные для компьютера:
• Математика целых чисел;
• Математика с плавающей запятой.
Первая использует значения типов CHAR, INTEGER, LONG и т. д. Вторая оперирует числами FLOAT, DOUBLE и т. п. Разница между двумя этими видами математики заключается в том, как представлены числа в памяти и какие именно числа - целые или дробные - принимают участие в расчетах. Целые числа представлены в компьютере непосредственно в двоичной форме, без какого-либо кодирования. Как вы знаете, они могут быть как положительными, так и отрицательными, и у них отсутствует дробная часть. С другой стороны, числа с плавающей запятой должны иметь десятичные части.
Но к чему такая забота по поводу чисел? Объясняю: ПК может выполнять математические вычисления весьма быстро, но «весьма быстро» еще не значит «достаточно быстро для видеоигр». Даже с математическим сопроцессором ПК до сих пор оставляет желать лучшего в режиме реального времени при работе с трехмерной графикой. Известно, что вычисления с целыми выполняются гораздо быстрее, чем с дробными числами.
Вы можете спросить: «Почему мы обязательно должны использовать числа с плавающей запятой?» Ответ заключается в том, что по природе того типа программирования, которым мы занимаемся (трехмерная компьютерная графика), мы оказываемся перед неизбежной необходимостью достижения максимальной точности в наших вычислениях. Это заставляет нас использовать и дробные числа тоже.
Вычисления с плавающей запятой пожирают так много времени из-за способа представления чисел, с которыми они оперируют. Эти числа не являются в полном смысле двоичными, напротив, они хранятся в специальном формате IEEE, в котором характеристика (целая часть) и мантисса (экспонента) представлены в жутко свернутой форме, и прежде чем число использовать в вычислениях, его нужно еще расшифровать. Числа — это только инструмент для представления игровых объектов. Если бы вы захотели, то могли бы разработать свои собственные форматы хранения десятичных чисел.
Это та область, где в бой вступает математика с фиксированной запятой. Например, можно представить число содержащим как целую, так и десятичную часть внутри отдельного целого. Как это сделать? Притворимся, что десятичные части существуют где-то внутри целого и что двоичные цифры слева являются целой частью числа, а все, что находится справа, это десятичная часть. Рисунок 18.3 должен помочь вам представить это.
Где именно вы поместите десятичную часть, зависит от вас. Важно, чтобы позиция десятичной точки и выбор базового типа данных удовлетворяли нашим потребностям. Я предлагаю использовать тип данных LONG, который имеет 32 бита точности, а десятичную точку поместить где-нибудь посередине. Скажем, отведем для десятичной части восемь младших битов, тогда целая часть займет 24 старших разряда. Этого более чем достаточно для наших потребностей, поскольку в видеоиграх не нужна очень высокая точность. Пары знаков после запятой будет вполне достаточно. Чтобы использовать математику с фиксированной запятой, нам необходимо только разобраться, как выполнять с ней несколько операций:
§ присваивание;
§ сложение;
§ вычитание;
§ умножение;
§ деление;
§ сравнение.
Прежде чем разобраться в способах осуществления этих операций, мы все-таки должны понять как объявить число с фиксированной запятой. Взгляните, это действительно трудно:
long fix_1,fix_2,fix_3;
Что, обманул вас на секунду, да? Не правда ли, проще не придумаешь? Как я сказал ранее, мы используем тип данных LONG для чисел с фиксированной точкой и только предполагаем наличие десятичной части. И это все, что нам нужно для определения числа с фиксированной запятой.
Присваивание
Теперь поговорим о присваивании. Если мы хотим присвоить целую часть фиксированного числа, мы делаем .следующее:
int а=300;
long fix_1=0;
// в двоичном виде - 0000 0000 0000 0000 0000 0000 0000 0000
fix_1=((long)a << 8);
Сложнее обстоит дело с присваиванием дробных чисел. Для этого мы должны использовать умножение с плавающей запятой и записать число в LONG. Ниже показано, как выполнить такую операцию присваивания:
long fix_1 = (long) (23.4*256)
Мы умножаем на 256 потому, что это эквивалентно сдвигу на восемь разрядов влево (помните, я уже говорил, что нет смысла сдвигать числа с плавающей запятой).
Сложение и вычитание
Чтобы складывать или вычитать числа с фиксированной запятой, мы можем использовать обычные операторы Си. К примеру, чтобы сложить два числа и записать результат в третье, мы могли бы сделать так:
fix_3=fix_1+fix_2
Вычитание получается точно так же. Кроме того, вы можете использовать и отрицательные значения, поскольку внутреннее представление типа данных LONG учитывает знак, и это вполне применимо также и к числам с фиксированной запятой.
Умножение
Самая сложная из всех операций — это умножение. Здесь мы должны соблюдать осторожность: существует несколько нюансов, которые могут наплодить множество ошибок. Например, когда умножаются два числа с фиксированной запятой, для сохранения результата может потребоваться в два раза больше битов, нежели содержат сомножители. Другими словами, если оба сомножителя были 32-битными, не исключена возможность получить 64-битный результат. То есть мы должны следить за величиной чисел с фиксированной запятой, которые перемножаются. Как и обычно, для этого мы будем использовать оператор умножения. Однако когда умножение выполнено, мы должны сдвинуть результат на 8 позиций назад вправо. Это нужно сделать потому, что когда мы задаем число с фиксированной запятой, то искусственно перемножаем его на 256 (помните, восемь младших разрядов заняты под десятичную часть).
Поэтому мы должны сдвинуть окончательный результат назад восемь раз вправо, иначе говоря, результат должен быть разделен на 256. Если этого не сделать, то, умножая 2 на 5, мы получим 2х5х256 вместо правильного ответа равного 10. Здесь приводится способ, каким делать умножение.
fix_1=(fix_2*fix_3)>>8;
Если вы хотите вычислить сумму произведений, то нет надобности после каждого умножения сдвигать результат. Достаточно сделать это только один раз в самом конце расчетов. Рассмотрим пример:
fix_1=(fix_2*fix_3+fix_4*fix_5)>>8;
Это свойство чисел с, фиксированной запятой могло бы пригодиться, если вы захотите оптимизировать приведенный фрагмент и дальше с целью избавиться от всех сдвигов.
Деление
При выполнении деления я предлагаю вместо использования символа деления умножать на обратную величину. Как правило, это несложно сделать. Запомните, что деление — всегда более медленная операция, чем умножение, независимо от того, применяете вы числа с фиксированной или с плавающей запятой. Здесь приводится пример того, как могло бы быть выполнено деление:
fix_1=(long)(256*1/34);
fix_2=(fix_3*fix_1)>>8 ;
Прежде чем мы перейдем к следующей теме, мне бы хотелось затронуть некоторые детали, о которых прочие авторы обычно не любят говорить. Это точность и максимальное цифровое представление.
Точность
Поскольку мы договорились, что в нашем формате чисел с фиксированной запятой восемь младших разрядов будут содержать десятичную часть, то самым маленьким числом, которое можно представить, окажется значение 1/256 или примерно 0.004. Следовательно, максимальная ошибка будет получаться при умножении двух чисел. Наибольшее число, которое мы можем получить в произведении, равно 32761. Следовательно, наибольшая ошибка, которая может закрасться в расчеты, это 0.004х32761 или 131.044. Ого! Это слишком много. Однако в действительности у нас никогда не будет ошибок такой величины. Только вы не должны с одним и тем же числом выполнять больше 2-5 умножений, и сомножители не должны превышать 32761.
Как правило, в большинстве случаев ошибки не будут превышать 0.01-0.5, что вполне допустимо, поскольку 90 процентов всех расчетов направлены на определение местоположения пикселей на экране и результаты все равно округляются.
Хватит насчет точности. Перейдем к определению максимального числа, которое может быть представлено в нашей системе с фиксированной запятой.
Максимальное цифровое представшие
Поскольку у нас есть 24 бита для целой и 8 бит для десятичной части числа, вы можете подумать, что таким образом можно представить значения вплоть до 224 или 16777216. Почти, но не совсем. Так как число с фиксированной запятой может быть и положительным и отрицательным, мы располагаем числами в диапазоне от -8388608 до +8388608. Мы можем без всяких проблем складывать и вычитать числа из этого диапазона, но при умножении, должны быть исключительно осторожны, чтобы не переполнить тип LONG.
Когда я изучал математику с фиксированной запятой в первый раз и пытался алгоритмизовать ее, то допустил ошибку. Я использовал схему, похожую на нашу (то есть 24 бита для целой части и 8 бит для десятичной) и думал, что наибольшие числа, пригодные для умножения, могут быть любыми, лишь бы результат укладывался в 24 бита. Это означало бы, что можно перемножить 4096 на 4096 и получить правильный ответ. Ошибочка! Я забыл об остальных 8 битах десятичной части- Следовательно, в действительности я умножил 4096х256х4096х256, что составляет примерно 1.09х1012. Поскольку тип LONG состоит из 32-х битов, то он может представлять числа от -2147483648 до +2147483648 (которые в 1000 раз меньше полученного результата). Мораль сей басни такова, что числа с фиксированной запятой остаются принадлежащими типу LONG, и если в них записать числа, интерпретируемые как LONG, то при умножении возникнет переполнение.
Наибольший результат умножения, который может быть получен в нашей системе с фиксированной запятой равен 32761 или 181 в степени 2. Число 181 было получено исходя из следующих соображений: это число, которое, будучи умноженным на 256 и возведенным в квадрат, не должно превышать размерности типа LONG (+2147483648).
Мы используем 256, так как у нас есть восемь двоичных цифр, a 28 равно 256.
Как, ни странно, но наибольшее число, которое может быть получено в результате умножения — это 32761, а величина чисел для сложения может достигать примерно 8000000? Ну что ж... где-то мы нашли, а где-то потеряли.
Чтобы помочь вам в экспериментах с числами с фиксированной запятой и увидеть некоторые их интересные свойства, я создал небольшую библиотеку и программу main() для демонстрации их использования (Листинг 18.2). Я предлагаю вам потратить немного времени, чтобы получить действительно целостное понимание сути чисел с фиксированной запятой, поскольку это очень важно и про это мало кто знает. Никогда не надо забывать, что десятичная запятая является воображаемой!
Листинг 18.2. Функции библиотеки системы с фиксированной запятой (FIX.C).
// ВКЛЮЧАЕМЫЕ ФАЙЛЫ ///////////////////////////////
#include
#include
// определим наш новый тип чисел с фиксированной запятой
typedef long fixed;
//ФУНКЦИИ ///////////////////////////////////////////
fixed Assign_Integer(long integer)
{
return((fixed)integer << 8);
} // конец функции присваивания целочисленного значения ////////////////////////////////////////////////////////////
fixed Assign_Float(float number)
{
return((fixed)(number * 256)};
} // конец функции присваивания значения с плавающей запятой ////////////////////////////////////////////////////////////
fixed Mul_Fixed(fixed fl,fixed f2)
{
return ((fl*f2) >> 8);
} //конец функции умножения ////////////////////////////////////////////////////////////
fixed Add_Fixed(fixed fl,fixed f2)
{
return(f1+f2);
} // конец функции сложения ////////////////////////////////////////////////////////////
Print_Fixed(fixed fl)
{
printf("%ld.%ld", f1>>8, 100*(unsigned long) (f1 & 0x00ff)/256);
} // конец функции вывода числа с фиксированной запятой
// ОСНОВНАЯ ПРОГРАММА //////////////////////////////////////
main(}
{
fixed f1,f2,f3;
fl = Assign_Float(15);
f2 = Assign_Float(233.45);
f3 = Mul_Fixed(f1,f2);
printf("\nf1:");
Print_Fixed(f);
printf("\nf2:");
Print_Fixed(f2) ;
printf("\nf3:");
Print_Fixed(f3) ;
} // конец функции main
Конечно, в собственных программах вы фактически не будете использовать функции сложения и умножения. Я поместил их здесь только для того, чтобы вы могли поэкспериментировать со свойствами чисел с фиксированной запятой.
Теперь перейдем к встроенному ассемблеру.
Встроенный ассемблер
Ассемблировать или не ассемблировать: вот в чем вопрос. Почти шекспировская задача и нам ее сейчас предстоит разрешить.
Как я говорил ранее, вы должны использовать MASM и встроенный ассемблер, только если в этом действительно возникает необходимость. Ведь все равно операции, которые вы выполняете, являются машинно-зависимыми по своей природе. Вам не нужен ассемблер ни для чего, кроме создания графики и звука. Ни в коем случае не стоит использовать ассемблер для реализации игровой логики и алгоритмов. Кроме того, если ассемблер вам необходим для увеличения быстродействия, применяйте вместо MASM встроенный ассемблер. Он проще в использовании и его применение отнимает меньше времени при разработке программ. Мы обсудим несколько методов, которые увеличат скорость выполнения ваших программ во много раз.
Теперь рассмотрим некоторые из функций, которые мы написали в предыдущих главах и оптимизируем их с помощью новых технических приемов.
Оптимизация рисования пикселей
Первое, что мы должны сделать, это насколько возможно оптимизировать по быстродействию функцию рисования пикселей. Ведь на ней базируются, все остальные графические построения! Давайте возьмем функцию Plot_Pixel() из Листинга 5.4 (вы найдете ее в пятой главе «Секреты VGA-карт») и поглядим можем ли мы оптимизировать ее дальше. Листинг 18.3 содержит новую функцию.
Листинг 18.3. Функция для быстрого рисования пикселей.
void Plot_Pixel_Fast(int x,int y,unsigned char color)
(
// Функция рисует на экране точку несколько быстрее
// за счет использования сдвига вместо умножения:
// 320 * у = 256 * у + 64 * у = у << 8 + у << 6
video_buffer[((у<<8}+(у<<6) )+х]=соlоr;
}
Все. С точки зрения Си эта функция уже оптимизирована настолько, насколько возможно. Я могу вам предложить только следующее:
§ Перепишите программу на встроенном ассемблере;
§ Не передавайте параметры;
§ Возможно, создайте справочную таблицу из 64000 элементов, каждый из которых содержит адрес видеобуфера, соответствующий координатам Х и Y.
Правда, я думаю, что использование справочной таблицы на самом деле только замедлит выполнение программы. Ведь операция индексирования таблицы может отнять больше времени, чем два сдвига и сложение, необходимые для вычисления адреса. Но вот первые два пункта заслуживают внимания. Начнем с использования глобальных переменных, заменяющих передачу параметров. Например, определим такие глобальные переменные:
int plot_x, plot_у, plot_color;
Тогда нашу функцию можно переписать, как показано в Листинге 18.4
Листинг 18.4. Другая версия функции построения пикселя.
void Plot_Pixel_Global(void)
{
video_buffer[((plot_y<<8) + (plot_y<<6))+plot_x]=plot_color;
}
Это уже будет работать значительно быстрее, но нам все равно перед вызовом нужно выполнить операцию присваивания для переменных plot_x, plot_у и plot_color. Следовательно, вопрос в следующем - отнимет ли операция присваивания глобальных переменных меньше времени, чем передача параметров, создание и удаление фрейма стека вместе со ссылками на параметры? Может да, а может и нет. Все зависит от ситуации. Но цель заслуживает того, чтобы попробовать испытать и этот метод.
Теперь перепишем процедуру на ассемблере. Результат показан в Листинге 18.5
Листинг 18.5. Версия функции рисования пикселя на ассемблере.
Plot_Pixel_Asm(int x,int у,int color)
{
_asm{
les di,video_buffer // загрузить регистр es значением
// сегмента видеобуфер
mov di,y // поместить в di у-координату пикселя
shl di,6 // умножить на 64
mov bx,di // сохранить результат
shl di,2 // умножить еще на 8 (итого, на 256)
add di,bx // сложить результаты
add di,x // прибавить х-компонент
mov al,BYTE PTR color // записать цвет в регистр аl
mov es:[di],al // нарисовать пиксель
}
}
Ладно, покончим с этим. Ассемблерный вариант работает всего лишь на 2 процента быстрее, чем версия на Си. На то есть две причины:
§ Во-первых, компилятор Си проделывает неплохую работу, транслируя программу в машинные коды. Хотя, как вы знаете, мы можем сделать это вручную, используя ассемблер;
§ Во-вторых, когда вы используете встроенный ассемблер, он сохраняет все регистры и позже восстанавливает их. Единственный способ избавления от этого - написать внешние ассемблерные функции с использованием MASM. В данном случае это вполне допустимо, поскольку мы оптимизируем такую важную операцию как построение пикселя.
Наконец, я хочу показать вам последний пример оптимизации, которая позволяет ускорить вывод на экран в два раза.
Оптимизация изображаемой линии
Игры типа DOOM и Wolfenstein 3-D не используют все известные техники трехмерных графических преобразований, как это делают, например, имитаторы полетов. В них применяются совершенно гениальные методы для создания трехмерных образов. Эти методы базируются на изображении большого количества линий, проходящих в одном направлении. Обычно рисуются обычно вертикальные или горизонтальные линии и только в удаленных предметах присутствуют диагональные прямые. Следовательно, мы должны научиться максимально быстро проводить горизонтальные и вертикальные линии. Сейчас мы поговорим о горизонтальных прямых.
Как мы проходили в пятой главе «Секреты VGA-карт», для изображения горизонтальной линии лучше всего применить функцию memcpy () (см. Листинг 5.6). Начальный и конечный адреса вычисляются из Х-координат крайних точек линии. Вернувшись назад к этому методу, приходится признать, что он не совсем совершенен, поскольку memcpyO для перемещения данных использует тип BYTE. Но профессионалы в программировании игр знают, что VGA-карта работает быстрее с данными типа WORD. Следовательно, нужно попытаться исправить этот недостаток и записывать в видеобуфер WORD вместо BYTE. Сейчас я собираюсь переписать функцию изображения горизонтальных линий, учитывая это пожелание.
Это не так легко, как кажется, потому что конечной целью должна быть забота о работоспособности. Вы видите, что когда вы пишите WORD в видеобуфер, вы в сущности строите два пикселя. Вы должны быть осторожны, принимая это во внимание во время рисования линий. Скажем, мы хотели изобразить линию, тянущуюся от точки (51,100) до точки (100,100). У нас получится линия, которая выглядит чем-то похожей на рисунок 18.4.
Завершим анализ изображения этой линии тем, что каждая конечная точка в действительности занимает один байт, а не два. Следовательно, мы должны написать программу, которая может управлять состоянием, когда конечные точки BYTE или WORD граничат. Программа из Листинга 18.6 делает это.
Листинг 18.6. Программа, изображающая горизонтальную линию и использующая для этого тип WORD (HLINEF.C).
H_Line_Fast(int xl,int x2,int у,unsigned int color)
{
unsigned int first_word, middle_word, last_word,line_offset,
index ;
// тестируем 1 бит начальной х-координаты
if ( (x1 & 0х0001))
{
first_word = (color << 8);
}
else
{
// заменить цвет в обоих байтах
first_word = ( (color<<8) | color) ;
}
// тестируем первый бит в х2
if( (х2 & 0х0001) )
{
last_word = ((color<<8) | color);
)
else
{
// поместить цвет только в старший байт
last_word = color;
}
// теперь мы можем рисовать горизонтальную линию,
// выводя сразу по два пикселя
line_offset = ( (у<<7) + (у<<5) ); // у*160, поскольку в линии 160 слов
// вычислить цвет в середине
middle_word = ((color<<8) | color);
// левая граница
video_buffer_w[line_offset + (x1>>1)]= first_word;
// середина линии
for (index=(x1>>1)+l, index<(x2>>l); index++)
video_buffer_w[line_offset+index] = middle_word;
// правая граница video_buffer_w[line_offset+(х2>>1)] = last_word;
}
В начале работы функция проверяет, находятся ли конечные точки на границе байта (BYTE)? Основываясь на результатах проверки, функция создает два слова (WORD): одно будет началом линии, а другое ~ концом. В зависимости от результатов начального теста эти два слова содержат либо один, либо два байта, представляющих цвет. Затем функция изображает линию. Это выполняется следующим образом:
§ Пишется первый WORD, соответствующий левой границе;
§ Создается цикл FOR, который предназначен для записи в WORD середины линии;
§ Пишется WORD, соответствующий правой границе линии.
Хотя эта функция почти в два раза длиннее, чем первоначальная Н_Line, она почти в два раза быстрее.
(Существуют, правда, небольшие накладные расходы при вычислении границы). Чтобы сделать ее еще быстрее, я мог бы переписать часть, которая изображает середину на встроенном ассемблере, но я думаю, что вы и сами сделаете это в качестве легкого упражнения.
ИТОГ
Мы узнали множество полезных вещей, и даже если вы никогда не начнете писать игры, определенно станете более умелым программистом, поскольку приобрели новые навыки. У вас появилось несколько мощных методов оптимизации. Только не думайте, что лучше этих методов не бывает или что предложенные способы оптимизации годятся на все случаи жизни.
Наше длинное путешествие в мир оптимизации подошло к концу, и я хочу закончить его приглашением в следующую, девятнадцатую главу, где мы, наконец, на примере игры Warlock применим все, чему уже научились.Итак, до следующей главы.
ТЕХНИКА СОЗДАНИЙ ПАРАЛЛАКСА
Вы, конечно, не раз обращали внимание, глядя из окна автомобиля, что близлежащие объекты перемещаются гораздо быстрее, чем удаленные. Этот эффект получил пугающее название — параллакс. В повседневной жизни вы настолько часто наблюдаете это явление, что, скорее всего, считаете его чем-то само собой разумеющимся.
Параллакс является только одним из многих эффектов нашего визуального восприятия. Другой хорошо известный эффект - это перспектива. Перспектива и параллакс вкупе с другими ощущениями, такими как равновесие и слух, формируют завершенную картину окружающей среды.
Что же такое параллакс и каким боком он связан с программированием игр? Параллаксное смещение - это технический прием, применяемый в видеографике, когда два или более слоя графического изображения перемещаются в одну сторону, но с различными скоростями. Такое относительное смещение слоев обеспечивает некие визуальные эффекты, необходимые для реалистичной имитации глубины пространства и движения.
Теория игр
Следующая тема не слишком тесно связана с видеоиграми, а, скорее, относится к стратегическим играм типа шахмат. Тем не менее, я хочу осветить ее, чтобы сообщить вам некоторые интересные идеи.
Теория игр является разделом математики, слишком трудным и слишком абстрактным, для того чтобы применять ее в видеоиграх. Она оперирует набором правил для вычисления оптимального решения или алгоритма, позволяющего игроку победить. Обычно это делается с помощью множества матриц, вероятностей и линейного программирования. Тем не менее, мы можем извлечь из теории игр немало полезного.
Как правило, требуется решить проблему путем оценки текущего положения или игровой ситуации, подразумевая, что игрок точно оценивает свои действия в данной точке в данный момент времени и далее старается улучшить свою позицию. Чтобы выполнить это на компьютере, нам надо:
§
Во-первых, мы создаем несколько видов целевых функций, которые могли бы оценивать нашу текущую позицию;
§ Далее мы пробуем применить новую тактику или изменить позицию с тем, чтобы посмотреть, улучшит ли это наше положение;
§ Если это достигается, мы выполняем это действие;
§ Если нет, испытываем следующую тактику.
Именно по такой схеме работают шахматные программы. Компьютер постоянно ведет подсчет своих собственных очков и очков противника, зависящих от текущего расположения фигур, и пытается найти наиболее оптимальный ход. Этот «мыслительный процесс» может в действительности идти на многих уровнях. Компьютер будет проверять каждую из возможностей: если я сделаю так, то мой противник мог бы предпринять то-то..., а если я поступлю эдак, соперник сделает следующее... Так может продолжаться до бесконечности, хотя человек может проиграть даже в том случае, если компьютер будет делать только два шага «проверка-действие».
В видеоиграх эта тактика может быть использована как самое высокоуровневое управление всеми остальными функциями. К примеру, каждую минуту или около этого, компьютер мог бы просчитывать ходы и оценивать, как он действует. Если все его ходы удачны, он мог бы действовать так и дальше. Однако, если какая-то попытка оказалась неудачной, значит, пришло время для пересмотра тактики.
Теперь все вместе: Демонстрационная программа работы с клавиатурой
Как теперь насчет того, чтобы собрать все написанное про клавиатуру в одну кучу? В Листинге 3.7 представлена демонстрационная программа, которая состоит из вызовов уже написанных в этой главе функций. Она показывает скан-коды нажатых клавиш и состояние клавиш Ctrl и Alt. Если вы нажмете Q, программа завершит свою работу.
Листинг 3.7. Демонстрационная программа работы с клавиатурой (KEY.C).
// ВКЛЮЧАЕМЫЕ ФАЙЛЫ ///////////////////////////////////////
#include
#include
#include
#include
#include
#include
// ОПРЕДЕЛЕНИЯ ///////////////////////////////////////////////
// битовые маски для управляющих клавиш
#define SHIFT_R 0х0001
#define SHIFT_L 0х0002
#define CTRL 0х0004
#define ALT 0х0008
#define SCROLL_LOCK_ON 0х0010
#define NUM_LOCK_ON 0х0020
#define CAPS_LOCK_ON 0х0040
#define INSERT_MODE 0х0080
#define CTRL_L 0х0100
#define ALT_L 0х0200
#define CTRL_R 0х0400
#define ALT_R 0х0800
#define SCROLL_LOCK_DWN 0х1000
#define NUM_LOCK_DWN 0х2000
#define CAPS_LOCK_DWN 0х4000
#define SYS_REQ_DWN 0х8000
// Значения скан-кодов. Внимание: каждая клавиша продуцирует только
// один скан-код, поэтому определения даны только для символов
// нижнего регистра. Например, одна и та же клавиша соответствует
// символу "1" (нижний регистр) и символу "!" (верхний регистр).
// Однако пользоваться надо все равно определением SCAN_1
#define SCAN_ESC 1
#define SCAN_1 2
#define SCAN_2 3
#define SCAN_3 4
#define SCAN_4 5
#define SCAN_5 6
#define SCAN_6 7
#define SCAN_7 8
#define SCAN_8 9
#define SCAN_9 10
#define SCAN_0 11
#define SCAN_MINUS 12
#define SCAN_EQUALS 13
#define SCAN_BKSPаааааааа 14
#define SCAN_TABааааааааа 15
#define SCAN_Qааааааааааа 16
#define SCAN_Wааааааааааа 17
#define SCAN_Eааааааааааа 18
#define SCAN_Rааааааааааа 19
#define SCAN_Tааааааааааа 20
#define SCAN_Yаааааааа 21
#define SCAN_Uаааааааа 22
#define SCAN_Iаааааааа 23
#define SCAN_0аааааааа 24
#define SCAN_Pаааааааа 25
# define SCAN_LFT_BRACKET 26
#define SCAN_RGT_BRACKET 27
#define SCAN_ENTERаааа 28
#define SCAN_CTRLааааа 29
#define SCAN_Aаааааааа 30
#define SCAN_Sаааааааа 31
#define SCAN_Dаааааааа 32
#define SCAN_Fаааааааа 33
#define SCAN_Gаааааааа 34
#define SCAN_Hаааааааа 35
#define SCAN_Jаааааааа 36
#define SCAN_Kаааааааа 37
#defane SCAN_Lаааааааа 38
#define SCAN_SEMIааааа 39
#define SCAN_APOSааааа 40
#define SCANJTILDEаааа 41
#define SCAN_LEFT_SHIFT 42
#define SCAN_BACK_SLASH 43
#define SCAN_Zаааааааа 44
#define SCAN_Xаааааааа 45
#define SCAN_Cаааааааа 46
#define SCAN_Vаааааааа 47
#define SCAN_Bаааааааа 48
#define SCAN_Nаааааааа 49
#define SCAN_Mаааааааа 50
#define SCAN_COMMAаааа 51
#define SCAN_PERIODааа 52
#define SCAN_FOWARD_SLASH 53
#define SCAN_RIGHT_SHIFT 54
#define SCAN_PRT_SCRNа 55
#define SCAN_ALTаааааа 56
#define SCAN_SPACEаааа 57
#define SCAN_CAPS_LOCK 58
#define SCAN_F1ааааааа 59
#define SCAN_F2ааааааа 60
#define SCAN_F3ааааааа 61
#define SCAN_F4ааааааа 62
#define SCAN_F5ааааааа 63
#define SCAN_F6ааааааа 64
#define SCAN_F7ааааааа 65
#define SCAN_F8ааааааа 66
#define SCAN_F9ааааааа 67
#define SCAN_F10аааааа 68
#define SCAN_Fllаааааа 133
#define SCAN_Fl2аааааа 134
#define SCAN_NUM_LOCKа 69
#define SCAN_SCROLL_LOCKа 70
#define SCAN_HOMEаааааааа 71
#define SCAN_UPаааааааааа 72
#define SCAN_PGUPаааааааа 73
#define SCAN_NUM_MINUSааа 74
#define SCAN_LEFTаааааааа 75
#define SCAN_CENTERаааааа 76
#define SCAN_RIGHTааааааа 77
#define SCAN_NUM_PLUSаааа 78
#define SCAN_ENDааааааааа 79
#define SCAN_DOWNаааааааа 80
#define SCAN_PGDWNааааааа 81
#define SCAN_INSааааааааа 82
#define SCAN_DELааааааааа 83
// LL=¦гLL /////////////////////////////////////////////
unsigned char Get_Ascii_Key(void)
{
// +ёыш т сєЇхЁх ъыртшрЄєЁv хёЄ№ ёшьтюы, ЇєэъЎш тючтЁр•рхЄ хую
// ASCII-ъюф. +ёыш ёшьтюыр эхЄ, тючтЁр•рхЄё 0.
if (_bios_keybrd(_KEYBRD_READY)) return(_bios_keybrd(_KEYBRD_READ)) ;
else return(0);
} // ъюэхЎ ЇєэъЎшш
////////////////////////////////////////////////////////
unsigned int Get_Control_Keys(unsigned int mask)
(ааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааааа
// ЇєэъЎш тючтЁр•рхЄ ёюёЄю эшх ы¦сющ єяЁрты ¦•хщ ъыртш°ш
return(mask & _bios_keybrd(_KEYBRD_SHIFTSTATUS));
}//ъюэхЎ ЇєэъЎшш ////////////////////////////////////////////////////////
unsigned char Get_Scan_Code(void) (
// ЇєэъЎш тючтЁр•рхЄ ёърэ-ъюф эрцрЄющ ъыртш°ш // шёяюы№чєхЄё тёЄЁюхээvщ рёёхьсыхЁ
_asm{
mov ah,01hааа ;ЇєэъЎш 01h - яЁютхЁър эрцрЄш ъыртш°ш
int 16hаааааа ;тvчют яЁхЁvтрэш
jz emptyааааа ;эхЄ ёшьтюыр - тvїюфааааааааа
mov ah,00hааа ;ЇєэъЎш 0 - яюыєўхэшх ёърэ-ъюфр
int 16hаааааа ;тvчют яЁхЁvтрэш
mov al,ahаааа ;яхЁхьх•рхь Ёхчєы№ЄрЄ шч L= т AL
xor ah,ahаааа ;юсэєы хь L=
jmp doneааааа ;Ёхчєы№ЄрЄ тючтЁр•рхЄё т L-
empty:
xor ax,axааа ;юсэєы хь AX
done:
} //ъюэхЎ рёёхьсыхЁэюую сыюър
} // ъюэхЎ ЇєэъЎшш
// +T=+T=L- ¦¦++¦L¦¦L ///////////////////////////////////////
void main(void)
{
unsigned char key;
int done=0;
unsigned int control;
_clearscreen(_GCLEARSCREEN);
while(!done)
{
_settextposition(2,0);
if ( (key = Get_Scan_Code()) ) printf("%c %dа ",key,key);
// яЁютхЁър эр эрцрЄшх Ctrl ш Alt
if (Get_Control_Keys(CTRL))
printf("\ncontrol key pressed");
if (Get_Cbntrol_Keys(ALT))
printf("\nalt key pressedааа ");
if (key==16) done=1); // 16 Ч ¤Єю ёърэ-ъюф ъыртш°ш Q
}// ъюэхЎ Ўшъыр while
} // ъюэхЎ ЇєэъЎшш main
Точки, линии и области
Все мы видели игры типа Asteroids, Spectre и Major Havoc. Многие из них имеют общие черты первых видеоигр — все они выполнены линиями и все они, как правило, плоские. Кстати, в предыдущих главах мы еще ничего не делали для рисования проекции кроме отображения точки.
Точки, линии, многоугольники и объекты в трехмерном пространстве
Как мы уже видели, точка в трехмерном пространстве имеет три координаты (x,y,z). Этой информации достаточно, чтобы ее однозначно определить в пространстве.Будет логично, если следующим объектом, который мы определим, станет линия. Линией называют отрезок, соединяющий две точки в трехмерном пространстве. Мы можем даже написать структуры данных, определяющие точку и линию.
Листинг 6.1. Определение точки и линии в трехмерном пространстве.
// структура, описывающая точку в трехмерном пространстве
typedef struct point_typ
{
float x,y,z // координаты точки
} point, *point_ptr;
// структура, описывающая линию в трехмерном пространстве
typedef struct line_typ
{
point start, end; // линия задается двумя точками
} line, *line_ptr;
Используя структуры из Листинга 6,1, давайте определим линию, которая начинается в точке (0,0,0) и идет в точку (100,200,300)
line linel;
linel.start.x = 0;
linel.start.у= 0;
linel.start.z = 0;
linel.end.x = 100;
linel.end.у = 200;
linel.end.z = 300;
Теперь мы имеем описание линии. Если мы захотим, то сможем создать трехмерный мир, состоящий из линий и точек, но это будет скучным и тоскливым занятием.
Нам нужен больший уровень абстракции для моделирования объектов, и для этого нам пригодятся многоугольники. Как вы поняли из четвертой главы, многоугольник - это множество вершин, соединенных отрезками прямых. Вершины определяют границы многоугольника. В трехмерном пространстве Многоугольники очень похожи на своих двухмерных собратьев. Попробуем определить трехмерный треугольник. Он может выглядеть так, как это изображено иа рисунке 6.2.
Как вы можете видеть, на листе бумаги весьма несложно представить трехмерный объект. Мы будем использовать для описания «диагональный вид». Позже,мы к этому еще вернемся, а сейчас важно понять идею.
Описать многоугольник довольно просто: мы применим старое определение многоугольника и просто добавим к нему несколько атрибутов для создания новой законченной структуры. В Листинге 6.2 показана такая структура.
Листинг 6.2. Определение трехмерного многоугольника.
// структура, описывающая многоугольник
typedef struct polygon_typ
{
int num_vertices; // число
вершин
vertices[MAX VERTICES]; // координаты
вершин
int color; // цвет многоугольника
}polygon, *polygon_ptr;
Как можно заметить, в структуре описаны вершины и цвета. Эти составляющие необходимы для правильного отображения. Теперь, когда у нас есть структура, описывающая многоугольник, следующим шагом будет определение объекта на основе многоугольников. На рисунке 6.3 продемонстрирован один из таких объектов.
Теперь мы можем добавить еще один уровень к нашему описанию. Объект - это набор многоугольников. Создадим структуру, которая бы поддерживала эту концепцию:
Листинг 6.3. Описание трехмерного объекта на основе многоугольников.
// структура, описывающая объект
typedef struct object_typ
{
int num_faces; // число
граней
polygon faces[max_faces]; // грани, представленные многоугольниками
float xo,yo,zo; // координаты объекта в пространстве
int visible; // виден ли объект на экране?
} object, *object_ptr;
Структура данных в Листинге 6.3 описывает объект, который образован , множеством многоугольников или поверхностей. Используя эти структуры данных и определения, мы можем создать несколько трехмерных объектов: космический корабль, планету и окружающее космическое пространство.
Чтобы поместить объекты в трехмерное пространство, мы должны знать их пространственное расположение. То есть мы должны определить значения хо, уо и zo для каждого предмета. Так же, как и в случае с двухмерными объектами (которые мы уже обсуждали в четвертой главе), пространственные объекты мы будем определять в собственных локальных системах координат (0,0,0). Затем, когда мы будем перемещать объект, мы его просто переведем в конечную позицию.
Для наших структур это будет точка (xo,yo,zo).
Решением этой задачи будет простой перенос каждой из точек объекта, так же, как мы это делали для двухмерных объектов. Мы можем проверить этот метод и для объемных фигур. Например, представим себе куб, с вершиной в точке (2,2,2) (см. рис. 6.4). Если мы посмотрим на куб, то увидим, что он состоит из восьми вершин и шести поверхностей. Используя наши структуры данных, мы можем описать куб как объект с шестью гранями. Проблема, возникающая в данном случае, состоит в том, что это не самый лучший способ описания объекта. Ведь любая поверхность ограничена четырьмя точками и каждая из этих точек является общей еще для двух поверхностей. Это значит, что описание избыточно.
Возможно, более удачной окажется структура данных, содержащая список вершин. В этом случае избыточности не возникает. Однако при этом структура станет более общей и сложной, поскольку:
§ Мы должны будем иметь указатели либо индексы, или то и другое вместе для ссылки на вершины, необходимые для построения геометрической фигуры. Это увеличивает время распознавания данных объектов;
§ Наши структуры могут использовать заранее определенные массивы для хранения вершин и многоугольников. Это неэффективно использует память. Массивы должны быть одного размера, так как, независимо от того, используем ли мы один элемент массива или весь массив, нам необходимо отводить место под максимальное число элементов.
Эти факты надо принимать во внимание, когда создаете структуры для трехмерных объектов. Таким образом, для наших целей структуры данных из Листингов 6.2 и 6.3 являются наиболее простыми для работы. Если же вы хотите создать набор реальных трехмерных структур, то должны использовать другую тактику.
В общем случае представление двух- и трехмерных объектов сильно зависит от игры, которую вы пишете, от размера используемой памяти и т. д. (Наша цель сейчас - понять механизмы трехмерной графики и рендеринга, а не поиск наиболее эффективных способов представления данных в компьютере.
Это зависит от используемых алгоритмов и структур данных.)
Просуммируем все вышесказанное:
§ Трехмерные объекты состоят из вершин;
§ Эти вершины соединяются поверхностями или многоугольниками, которые задают границы объекта;
§ Объекты описываются относительно начала координат;
§ Существует много способов представления трехмерных объектов и вы должны выбрать тот, который устраивает вас по скорости и объему памяти.
Точки
Мы уже дали определение точке. Она представляет собой позицию на плоскости, которую можно описать парой координат X и Y. Давайте напишем маленькую программу на Си, рисующую точки на экране. Листинг 4.1 показывает такую программу.
Листинг 4.1. Программа, рисующая точки (POINTY.C).
#include
#include
void main(void)
{
int х, у, index, color;
// перевести компьютер в графический режим
_setvideomode(_VRES16COLOR); // режим 640х480, 16 цветов
// нарисовать 10000 точек на экране, расположенных случайным образом
for(index = 0; index<10000; index++)
{
// получить случайные координаты и цвет
х = rand()%640;
у
= rand()%480;
color = rand()%16;
_setcolor(color); // установить цвет для рисования точки
_setpixel(х,у); // нарисовать точку
} // конец цикла for
// ждать нажатия клавиши
while(!kbhit()){}
// восстановить текстовый режим
_setvideоmоde(_DEFAULTMODE) ;
} // конец функции main
Теперь разберемся, что делает эта программа:
Компьютер переводится в режим VGA с помощью вызова функции Си _setvideomode ( VRES16COLOR). Это функция из графической библиотеки Microsoft. После этого программа входит в главный цикл. В структуре
FOR каждый раз случайным образом генерируются три числа: одно для цвета и два других для координат (х,у) позиции точки, которую мы хотим нарисовать:
§ Затем мы используем библиотечную функцию _setpixel(х,у), чтобы нарисовать точку на экране. Программа делает это 10000 раз, а потом останавливается;
§ Затем программа ждет нажатия любой клавиши, после чего происходит выход в DOS.
Если вы запустите программу несколько раз, то сможете заметить, что точки все время оказываются в одних и тех же местах. Как это получается? Дело в том, что мы пользуемся функцией rand (), которая не является в полном смысле генератором случайных чисел. Она возвращает так называемые псевдослучайные числа. Чтобы избежать этого, вам надо всякий раз при запуске устанавливать
генератор случайных чисел с разными начальными значениями. Вставьте в начало программы функцию srand(int) — и все будет в порядке,
Трансляция объектов
Трансляцией объекта будем называть его перемещение, при котором не меняется ни угол поворота, ни размер объекта. Давайте воспользуемся нашей структурой данных для определения конкретного объекта, с которым будем экспериментировать и в дальнейшем. К примеру, пусть это будет астероид. На рисунке 4.7 показан его внешний вид. Листинг 4.5 содержит фрагмент, описывающий наш астероид.
Листинг 4.5. Описание астероида.
Object asteroid;
// определим поля
asteroid.num_vertices = 6; //шести вершин будет достаточно
asteroid.color
= 1; //цвет астероида - синий
asteroid.х0 = 320; // поместить астероид в центр экрана
asteroid.у0 = 240;
//теперь задаем координаты вершин как смещения относительно точки х0, у0
asteroid.vertices[0].х = 4.0;
asteroid.vertices[0].у = 3.5;
asteroid.vertices[1].х = 8.5;
asteroid.vertices[1].у = -3.0;
asteroid.vertices[2].x = 6;
asteroid.vertices[2].у = -5;
asteroid.vertices[3],x = 2;
asteroid.vertices[3].у = -3;
asteroid.vertices[4].х = -4;
asteroid.vertices[4].у = -6;
asteroid.vertices[5].х = -3.5;
asteroid.vertices[5].у = 5.5;
Конечно, в настоящих играх вам не придется так определять все свои объекты. Напротив, вы можете загрузить координаты вершин из файла или сгенерировать их (например, AutoCad использует формат DXF, содержащий списки вершин вместе с другими свойствами объекта; после того как DXF-файл загружен, координаты вершин считываются из него в соответствующие структуры). Но поскольку мы создаем всего один астероид, то можно описать его и вручную.
Теперь давайте чуть-чуть подумаем. Мы можем нарисовать вершины объекта относительно его положения на экране, которое описывается как (хо,уо). Если же мы хотим передвинуть объект, то можно сделать так:
x0=x0+dx
y0=y0+dy
где dx и dy — это количество пикселей, на которое мы хотим переместить объект по оси Х или Y.
Это все, что можно сказать о трансляции объектов. Теперь поговорим о масштабировании.
Трассировка луней
Трассировка лучей - это метод, применяемый для создания реалистичных образов на компьютере, используя полные модели трехмерного мира. Трассировка лучей решает множество проблем. Этот алгоритм может выполнять следующие действия:
§
Удаление невидимых поверхностей;
§ Перемещение;
§ Отражение;
§ Рассеяние;
§ Окружающее освещение;
§ Точечное освещение;
§ Наложение теней.
Изначально этот алгоритм разрабатывался для решения проблемы удаления невидимых поверхностей. Трассировка лучей создает образ, исходя из тех же законов, что и наше зрение. На рисунке 6.19 изображено некоторое пространство, которое может быть просчитано с помощью алгоритма трассировки лучей. Вы видите несколько объектов: источник света, наблюдателя и план наблюдения.
Чтобы воспользоваться трассировкой лучей для создания натуральных образов, нам придется использовать миллиарды световых лучей из источника света, и затем рассматривать каждый из них, надеясь, что он попадет в план наблюдения и примет участие в создании образа. Возникает вопрос - а зачем трассировать каждый возможный луч? На самом деле, нас интересуют только те лучи, которые достигают плана просмотра.
Запомнив это, давайте попробуем трассировать лучи в обратном направлении. Проследим движение лучей для каждого из пикселей на экране, а затем посмотрим, где эти лучи пересекаются с планом просмотра. Отметив пересечение, мы останавливаемся и окрашиваем соответствующий пиксель в нужный цвет. Это называется первичной трассировкой лучей.
Данная техника позволяет создавать трехмерные образы, но при этом не видны такие эффекты, как тени, рефракция и рефлексия. Чтобы воссоздать перечисленные эффекты, мы должны принять в рассмотрение специальные вторичные лучи, которые исходят из точек пересечения.
Это все делается рекурсивно до достижения некоторого уровня детализации. Затем полученные по всем лучам результаты складываются и соответствующему пикселю присваивается вычисленный цвет.
Трассировка лучей - это один из наиболее насыщенных вычислениями методов расчета трехмерных изображений, но зато и результаты получаются впечатляющими. Есть только одна проблема: для решения этой задачи в реальном времени не хватает мощности даже самого быстродействующего компьютера. Потому нам придется учесть данное обстоятельство и применить идею трассировки лучей для создания другого метода. Он будет более ограничен, но позволит нормально работать с трехмерными мирами на обычном ПК. Исходя из этого, мы попробуем реализовать упрощенный вариант трассировки лучей, используя только первичные лучи для генерации изображения. С последующими оптимизациями возможно достижение достаточно высокой производительности. Если вам интересно узнать, как это можно сделать, то стоит продолжить чтение нашей книги.
Трехмерное звездное небо
Техника трехмерных спрайтов, которую мы обсуждаем, используется не только в играх типа Wing Commander, но также и в DOOM, и в Wolfenstein. Если вы поиграете в DOOM, то заметите, что гуляющие или преследующие вас монстры (спрайты) изображаются в разных видах. В шестой главе, «Третье измерение», мы обсуждали метод трассировки лучей и метод представления объектов с помощью многоугольников. Теперь я хотел бы снова вернуться к этому и показать, как можно создать трехмерное космическое пространство внутри которого и будут происходить наши звездные войны.
Теоретически создание трехмерного звездного неба тривиально. Мы могли бы просто задать несколько миллионов случайных точек в трехмерном пространстве, использовать аксонометрическую проекцию для их визуализации и, нарисовать их по пикселям. Этот метод волне применим и обычно используется в симуляторах. Однако у него есть ряд недостатков: он работает медленно, требует много памяти и получаемое в результате изображение быстро надоедает из-за своего однообразия. Но поскольку мы с вами сейчас занимаемся оформлением игры, то должны сделать так, чтобы экран действительно стал трехмерным звездным небом. Но мало сказать, надо еще и сделать! Если вы найдете время и посмотрите Star Trek или подобный симулятор, то увидите звездное небо, созданное с помощью одиночных пикселей (звезд). Эти звезды расположены на экране случайным образом и передвигаются к периферии вначале медленно, а потом все быстрее и быстрее. Это происходит до тех пор, пока звезды не будут отсечены, выйдя за пределы обозримого пространства.
Удачный угол зрения на спрайты
Эта тема наиболее сложная из всех, которые мы до сих пор рассматривали. При перемещении трехмерных спрайтов по экрану, с точки зрения наблюдателя все должно выглядеть так, словно объекты и в самом деле имеют три измерения. Наблюдатель в нашем случае — это игрок, и его позиция фиксирована. Обычно она имеет координаты (0,0,0) или слегка сдвинута по оси Z. В любом случае, когда спрайт, к примеру, делает поворот, и мы хотим, чтобы он выглядел как настоящий трехмерный объект, нам следует менять кадры этого спрайта на экране в последовательности, соответствующей реальной смене ракурсов «живого» объекта. (Такую фразу и не выговоришь на одном дыхании!)
Полное и элегантное решение этой проблемы слишком... масштабно для нас. Нам всего лишь требуется, чтобы наши программы, модели и алгоритмы. давали реалистичные результаты. Поэтому, алгоритм, который мы обсудим в этом разделе, едва ли решит поставленную задачу в полном объеме, и в будущем, вы наверняка внесете в него свои дополнения. Однако он является хорошей стартовой площадкой, а я всегда предпочитаю иметь хороший фундамент для последующего строительства, а не слабое его подобие, на котором после ничего и не соорудишь.
Как и при написании любой программы для любого компьютера, мы должны вначале четко определить задачу, а затем обдумать возможные пути ее решения. • Итак, проблема: как выбрать кадр для изображения объекта на основании угла между лучом зрения игрока и направлением «взгляда» самого объекта.
Замечание
В общем случае объект может передвигаться и не в том направлении, куда обращена его лицевая сторона, Мы не будем сейчас заострять внимание на этом варианте движения, чтобы чрезмерно не усложнять нашу задачу. Пока будем считать, что объект всегда движется в ту сторону, куда он обращен лицом. Это предположение вполне обосновано, так как в программе, которую мы впоследствии напишем, будут участвовать космические корабли с кормовыми дюзами. Точно так же и в играх типа Wolfenstein или DOOM игровые персонажи обычно движутся в том направлении, куда они смотрят (или в обратном, если они пятятся).
Попытаемся вначале проанализировать проблему. Нам необходимо определить вид объекта, который зависит от направления взгляда игрока и траектории объекта или направления его движения. Как мы уже говорили, луч зрения игрока можно зафиксировать и считать, что он всегда перпендикулярен экрану. Тогда нам нужно будет побеспокоиться только о векторе траектории объекта, выводимого на экран. На рисунке 8.7 изображена взаимосвязь между вектором направления взгляда игрока и некоторой траекторией передвижения объекта.
Теперь мы должны сделать вот что: возьмем игрушечную машинку или что-нибудь подобное и будем передвигать ее перед собой (шум мотора имитировать при этом не обязательно, можно все делать тихо). Проделав это, вы быстро придете к выводу, что рисуемое на экране изображение космического корабля, движущегося прямолинейно, практически одинаково для всех параллельных траекторий независимо от местоположения объекта. Конечно, это справедливо только частично, зато мы получили хорошую отправную точку для нашего первого алгоритма выбора правильного кадра.
Что же мы должны сделать:
§ Вычислить угол между траекторией движения объекта и лучом зрения игрока (который всегда направлен прямо в экран);
§ Разделить полученный угол на квадранты. Затем на основании полученного индекса выбрать наиболее подходящее изображение среди предварительно подготовленных оцифровкой фотографий модели или нарисованных в графическом редакторе. (Более подробно это обсуждается в разделе «Оцифровка объектов и моделирование».)
§ Вывести на экран подходящий кадр, используя аксонометрическую проекцию и масштабируя объект до необходимого размера.
В результате на экране получается реалистичная картина.
Каким же образом находится угол между траекторией объекта и лучом зрения наблюдателя? Ответ может быть получен с помощью скалярного произведения векторов.
Мы знаем, что угол между двумя векторами можно найти с помощью скалярного произведения векторов, как это показано на рисунке 8.8.
Формула 8.4. Вычисление угла между наблюдателем и объектом.
Если мы зададим вектор направления взгляда, как V, а вектор скорости, как О, тогда угол между ними можно будет найти по следующей формуле:
Пусть V = (vx,vy,vz) и О = (ox,oy,oz), тогда
Если бы мы хотели сформулировать это действие словами, то могли бы сказать так: «Угол между V и О равен арккосинусу скалярного произведения этих векторов, разделенного на произведение длин векторов».
Угол между V и О, рассчитанный по этой формуле, имеет одну особенность: он всегда внутренний, то есть больше 0, но меньше 180 градусов. Следовательно, один и тот же результат, полученный по этой формуле, может соответствовать двум разным углам. Это происходит потому, что скалярное произведение не дает информации о направлении вектора (или о направлении, в котором вы отсчитываете положительный угол). Другими словами, эта формула всегда выдает наименьший из углов между двумя векторами. Если вы будете помнить об этом, то такое поведение данной формулы не будет большой проблемой. (Это напоминает бутерброд, который всегда падает маслом вниз. Если вы не знаете об этом, то такой результат может свести вас с ума. А кто предупрежден, тот вооружен.)
Рисунок 8.9 иллюстрирует указанную проблему графически. На этом рисунке показан вектор направления взгляда, три возможных положения вектора траектории и полученный в результате расчетов по формуле 8.4 угол.
Кстати, формулу 8.4 можно значительно упростить, вспомнив, что нас интересует только плоскость X-Z, так как луч зрения всегда перпендикулярен плоскости просмотра.
Но как же, в конце концов, определить действительный угол? Конечно, вы Могли бы воспользоваться еще и векторным произведением, чтобы решить, корректен ли угол, полученный в результате расчетов по формуле 8.4 или необходимо увеличить его еще на 180 градусов. Однако я слишком не люблю математику (возможно, именно поэтому я и доктор математических наук) и предпочитаю вместо грубой силы использовать тонкую интуицию.
Если мы сообразим, что вектор траектории объекта имеет ту же исходную точку, что и вектор направления взгляда, а затем проверим, в какой из полуплоскостей относительно луча зрения расположен Х-компонент вектора траектории, то мы сможем определить, больше или меньше 180° искомый угол. Это наглядно изображено на рисунке 8.10.
Применяя метод проверки Х-компонента, мы можем написать простую функцию, которая вначале рассчитывает угол, используя скалярное произведение, а затем проверяет, находится ли координата Х справа (положительная) или слева (отрицательная) от вектора направления взгляда. Если координата Х положительная, мы вычитаем угол, полученный с помощью формулы 8.4 из 360 градусов (это все равно, что прибавить 180). Затем мы можем взять рассчитанный угол и разбить его на 12 квадрантов (либо взять его модуль по основанию 12). Полученное число затем можно использовать как индекс для нахождения кадров спрайта. (Конечно, кадры должны быть расположены в правильном порядке, то есть кадрам, полученным при вращении объекта против часовой стрелки с шагом в 30 градусов, должны соответствовать индексы от 0 до 11. При этом нулевой индекс должен указывать на кадр объекта, повернутого тыльной стороной к наблюдателю.)
Если значение координаты Х отрицательное, происходит то же самое за исключением того, что будет использован другой банк изображений, и оперировать потребуется с абсолютным значением X.
Кадры, которые я создал для демонстрации этого алгоритма, расположены в файле VRYENTXT.PCX. Они расположены слева направо и сверху вниз. Каждая картинка содержит изображение, повернутое на 30° против часовой стрелки, а в исходной позиции нос корабля направлен прямо в экран (или, с точки зрения игрока, корабль обращен к нему тыльной стороной). Этот же файл мы использовали и в предыдущем примере.
Демонстрационная программа будет использовать рассчитываемые углы для выбора кадров. Но мы же не можем поместить корабль просто в пустоту. Это будет скучно! Нам надо добавить что-нибудь для оживления картинки.Я предлагаю создать трехмерное звездное небо. Под трехмерностью я здесь понимаю то, что звезды будут перемещаться к вам или от вас, а не влево или вправо, как это мы делали раньше, Надо отметить, что космический корабль, летящий в звездном пространстве, выглядит превосходно. Однако следует еще поговорить о том, как же создается такое трехмерное звездное небо.
Удаление невидимых поверхностей
Среди программистов, работающих в области компьютерной графики, техника удаления невидимых поверхностей считается «высшим пилотажем». Известно, что удаление невидимых поверхностей является сложной математической задачей. Существует довольно много алгоритмов для ее решения, но все они очень сложны в реализации и редко работают с приемлемой для видеоигр скоростью.
Удаление невидимых поверхностей можно разделить на две фазы:
Фаза 1. Прежде всего удаляются поверхности, которые никогда не будут видны с точки зрения наблюдателя. Для этого мы должны использовать точку пересечения между вектором наблюдения и вектором нормали к каждой из рассматриваемых плоскостей. Мы вычисляем это значение. Если значение угла меньше 90°, то поверхность видна, если больше 90° - нет и она удаляется.
Фаза 2. После того как скрытые поверхности удалены, видимые плоскости должны быть окрашены. Вы должны быть уверены, что в течение этой фазы, объекты будут выглядеть правильно.
Существует популярный Алгоритм Художника, который это хорошо умеет делать. Он работает, выполняя пять тестов для каждой видимой пары многоугольников (поверхностей) и затем создает последовательность их окраски от дальней части изображения к ближней.
Другая техника создания этой последовательности носит название Алгоритма Z-буфера. Он работает в пространстве образа на уровне пикселей. Он прост в реализации, но медленно работает и требует много памяти.
Настоящие разработчики видеоигр никогда не используют эти алгоритмы в чистом виде. Мы должны создать свою систему», которая будет сочетать оба метода. Для этого детально рассмотрим каждый из них.
Удобные детали и инструменты
На что вам в первую очередь необходимо обратить внимание при выборе графической программы? Существует не так уж и много возможностей графических редакторов, которые я использую наиболее часто. Я с удовольствием помогу вам и укажу на них:
§
Посмотрите, есть ли в программе возможность импорта и экспорта различных графических форматов. Существует целое семейство битовых графических форматов, и у вас должна иметься возможность отконвертировать любое изображение в тот формат, который вы в конечном счете используете. К примеру, вы можете решить, что ваши окончательные картинки должны сохраняться в PCX-файлах, но нашли несколько интересных изображений в GIF-, BMP- или TIF-формате. Было бы прекрасно иметь возможность преобразовать их в выбранный формат, при необходимости отредактировать и сохранить в новом PCX-файле. Эти четыре формата наиболее часто употребляются графическими редакторами, поддерживающими 256-цветный режим;
§ Обратите внимание на способ управления цветами и их расположение в палитре. У вас может возникнуть необходимость собрать окончательное изображение из нескольких исходных картинок, и вам потребуется создать палитру, которая будет обеспечивать наилучшие результаты с различными типами изображений. Вы можете обнаружить, что комбинирование этих изображений приводит к перемешиванию палитры. Возможность перестановки цветов по яркости оттенков является удобной деталью. Мы еще вернемся к этой возможности и рассмотрим ее чуть позже в данной главе;
§ Очень хорошо, если программа позволяет уменьшить количество цветов в изображении. Вы быстро обнаружите, что 256 цветов вам хватит ненадолго, если вы работаете с несколькими фотографиями или набором текстур. Вам может потребоваться уменьшить количество цветов, используемое каждым изображением, до 64 или даже до 32;
§ Очень хорошей особенностью некоторых программ является способность комбинировать палитры двух и более изображений и подбирать лучшие цвета для объединенного изображения.
После сокращения палитр отдельных изображений (как это было описано в предыдущем пункте) вы сможете собрать несколько, меньших палитр в одной. Если вы не найдете такую программу, воспользуйтесь редактором, поддерживающим 24-разрядную графику (реальные цвета). Этот тип программ, позволит вам отрезать, и приклеивать различные изображения в одном файле, а затем преобразовать собранное изображение в вашу лучшую 256-цветную палитру;
§ Вам также может потребоваться возможность создания плавных цветовых переходов. Существует множество таких программ и некоторые из них работают с двумя цветами, плавно изменяя окраску от одного оттенка к другому. Иные графические программы позволяют выбрать диапазон цветов в палитре и произвольно смонтировать их. Я предпочитаю последний метод, поскольку он дает наиболее хороший результат и особенно удобен для раскраски битовых образов. Одна из программ, обладающих таким достоинством, называется Electronic Art's Deluxe Paint;
§ Наконец, вы будете очень довольны, если сможете выполнять сглаживание. Это такой технический прием, который позволяет убирать неровности внешнего вида скошенных или закругленных областей путем смешивания промежуточных цветов в шероховатых местах. Если вы используете эту особенность осторожно, то можете сгладить цветовой переход в наиболее контрастных участках и придать изображению более натуральный внешний вид.
Однако существует ряд важных «инструментальных средств», которые невозможно купить ни за какие деньги. Это желание, терпение и хороший художественный вкус. Доведение до совершенства любой задачи требует практики, будь то программирование, кулинария или рисование. Не отчаивайтесь, если ваше первое творение окажется совсем не таким, каким вы его себе представляли. Считайте, что, сделав это, вы уже чему-то научились и двигайтесь вперед.
Теперь давайте рассмотрим основы создания игровой графики.
Уклонение
Пока мы еще не ушли слишком далеко в наших рассуждениях, продолжим нашу Дискуссию разговором о явлении прямо противоположном преследованию — об уклонении. Чтобы сконструировать создание, уклоняющееся от игрока, нам нужно сделать в точности противоположное предыдущим действиям. Алгоритм 13.2 в общих чертах показывает это.
Алгоритм 13.2. Алгоритм Уклонения.
//пусть (рх,ру) - позиция игрока и (ех,еу) - позиция противника
whilе(игра) {
.....// код программы
// Вначале - горизонтальная составляющая перемещения
if ex>px then ex=ex-1
if ex
//Теперь - вертикальная составляющая
if ey>py then ey=ey-1
if ey
.... // код программы
В этом месте я еще раз хочу заострить ваше внимание на том, что игры не думают. Персонажи в них только совершают несложные эволюции в пространстве и времени, да определенным образом реагируют на окружающую обстановку и действия игрока. (Некоторые ученые считают, что люди - это не что иное, как набор действий и реакций.)
А теперь перейдем к следующей теме и обсудим такое понятие как «шаблонные мысли».
Улучшения WarEdit
WarEdit уже будет работать вполне удовлетворительно, но на полноценный редактор он все же не тянет. В нем недостает многих деталей, необходимых для настоящей работы. Сейчас я хочу сказать о некоторых моментах, которые вы могли бы доработать сами:
§ Использование разноцветных точек для представления игровых объектов слишком примитивно. Более серьезным и удобным решением было бы использование окна редактирования с возможностью прокрутки, в котором каждый элемент игрового пространства представлен собственной уменьшенной копией;
§ В нашем редакторе любой объект занимает всю ячейку игрового пространства целиком. Но что если мы хотим поместить два мелких предмета в одно и то же место? Здесь могло бы пригодиться окно детализации ячейки;
§ А как быть насчет спецэффектов типа освещения? Неплохо было бы иметь возможность изменять уровни освещения в целом;
§ Еще не помешало бы добавить возможность указания начальных и конечных точек перемещения (телепортации) игрока;
§ Важный технический прием, не примененный в WarEdit, называется полиморфизмом. Эта методика позволяет, используя одну и ту же ссылку, обращаться к различным типам данных. Все структуры данных и определения обычно жестко закодированы в программе и их необходимо импортировать в игру в виде включаемого файла. Но гораздо лучше иметь некоторое подобие синтаксического анализатора, который будет считывать все игровые объекты и значения из файла инициализации.
Можно было бы и дальше продолжить список усовершенствований редактора. Обычно разработчики так и поступают, пока не получат окончательную версию. Фактически, мы могли бы потратить несколько дней и улучшить редактор до уровня Wolfenstein 3-D. (Добавление в него деталей из DOOM отняло бы месяцы). В любом случае, я думаю, у вас уже есть множество идей на этот счет, и если ваш первый редактор окажется вдвое лучше WarEdit'a, то с ним, вероятно, вы создадите и вдвое лучшую игру.
Уменьшение проекционных искажений
Проблема, о которой нам надо поговорить, заключается в проекционном искажении. Как вы знаете, для реализации отсечения лучей мы нарушили правило и использовали одновременно полярные и декартовы системы координат. Это привело к эффекту «рыбьего глаза» (то же самое возникает,когда вы смотрите сквозь сильную линзу).
Это сферическое искажение возникает вследствие использования нами при трассировке лучей радиального метода. Мы рассчитываем все лучи, выходящие из одной точки (позиции игрока). Сферические искажения возникают потому, что все объекты, с которыми пересекаются лучи, определены в «прямоугольном» пространстве. Расчет же лучей проводится в «сферическом» или «полярном» пространстве. Пример такого пространства представлен на рисунке 6.27.
Теперь посмотрим на рисунок 6.28. Мы увидим, что наблюдает игрок, когда смотрит прямо на стену. Он видит прямоугольник. Но так как расстояния до точек пересечения различны, изображение получается искаженным. Рисунок 6.29 показывает два результата отсечения лучей. Первый построен с учетом компенсационных искажений, а второй — без их учета. Все это очень интересно, но как это реализовать? Ответ прост: нужно умножить функцию масштаба на инверсную функцию. Синусоидальное искажение может быть компенсировано умножением масштаба на cos-1
текущего угла по отношению к полю наблюдателя (60 градусов). То есть мы должны умножить каждое значение угла от -30 до +30 на cos-1
того же угла. Это исключит искажение.
Универсальный асинхронный приемопередатчик
ПК оборудованы универсальным асинхронным приемопередатчиком (UART) - чипом, который принимает и передает последовательные данные. Существуют Два наиболее популярных UART для ПК:
§
Модель 8250;
§ Модель 16550.
Можете считать, что они полностью совместимы друг с другом и нам не нужно выяснять, какой из них используется. Единственным их важным отличием является только то, что модель 16550 имеет внутренний FIFO (First In, First Out - "первый вошел - первый вышел") буфер, который располагает входящие - данные так, что они не могут потеряться вследствие задержки обработки. Теперь взглянем на каждый из регистров UART и на то, как получить к ним доступ. После того как мы обязались написать полную библиотеку для связи, необходимо уяснить, как открыть последовательный порт, а также как осуществлять чтение и запись. Написав однажды,соответствующие функции, мы можем сконцентрироваться на целях игры.
Управление приоритетным состоянием
Можно еще усовершенствовать наш КА, если ввести управление сменой состояний с помощью некоторых переменных и функций. На практике это означает возможность изменения текущего режима, не дожидаясь полного завершения программы, отвечающей за его выполнение.
В имитаторе движения «Мухи» каждое состояние выполнялось до полного завершения. Но если включить в программу проверку выполнения или невыполнения некоторых условий, это позволило бы КА «выпрыгнуть» из состояния, не дожидаясь окончания его «отработки».
Этим заканчивается наш разговор о конечных автоматах, которые могут быть использованы в наших играх для моделирования поведения существ и придания им видимости наличия интеллекта. Теперь к имеющейся у нас системе высокоуровневого управления не помешает добавить низкоуровневое функционирование.
Уравнение плоскости
Я говорил, что мы можем использовать уравнение плоскости для многоугольника, для нахождения значения Z-компонента каждого из пикселей внутри преобразуемого прямоугольника. Вот это уравнение,
Дано: Точка (х,у) и вектор нормали к многоугольнику
|
|
Nz
Z = ---------------------------
1- Nx * X – Ny * Y |
|
|
Ускорение процесса двоичного кодового преооразовзния (бит-блиттинга)
Если подумать о типах разрабатываемых нами компьютерных игр, то станет очевидно, что основной упор нам придется делать на графику.
При работе с такими играми (трехмерные игры, подобные Wolfenstein 3D или DOOM) компьютер большую часть времени тратит на прорисовку стен в трехмерном пространстве или на изображение двухмерных образов спрайтов, представляющих игровые объекты. Мы обсудили изображение в трехмерном пространстве в шестой главе, «Третье измерение». Поэтому теперь я хотел бы остановиться на вопросе оптимизации вывода спрайтов, поскольку это нам особенно понадобится при создании игр данного типа.
В предыдущих главах мы разобрались с понятием бит-блиттинга (глава пятая, «Секреты VGA-карт») и рассмотрели, как передается растровое изображение из одной области памяти в другую. Держу пари, что 90 процентов времени при создании игр уходит на то, чтобы придумать, как ускорить эти процессы. В восемнадцатой главе, «Техника оптимизации», мы обсудим общую теорию оптимизации и разберем несколько примеров использования соответствующей техники после завершения работы над игрой. (Не забывайте о том, что все оптимизационные трюки следует применять на заключительной стадии разработки игры!) Вы должны четко представлять, что для каждой новой игры необходимо заново создавать и реализовывать подходящие алгоритмы битового замещения; более того, вы можете исгюльзовать от двух до пяти различных способов бит-блиттинга в одной и той же игре. При этом каждый из них будет предназначаться для вполне конкретного случая. Например, вы можете обнаружить, что можно оптимизировать функцию бит-блиттинга для объектов, которые на протяжении всей игры остаются неподвижными. Если это так, создайте два преобразования: одно — для движущихся объектов, другое — для стационарных. Код для каждого процесса бит-блиттинга занимает примерно один-два килобайта ияи даже меньше.
Рассмотрим еще один пример возможной оптимизации. Естественно, что чтение данных вашей функцией бит-блиттинга определенным образом зависит от способа их хранения.
И возможно, преобразование будет происходить быстрее, если представить данные в другом виде. Например, вы можете потерять эффективность, сохраняя значения пикселей построчно (в виде двухмерной матрицы), а не по столбцам (по 16 бит). Если иная форма хранения данных дает выигрыш в производительности для некоторых из ваших алгоритмов бит-блиттипга, напишите для них процедуру предварительного преобразования этих данных.
В любом случае, думайте о том, как максимально ускорить процесс бит-блиттинга. Ведь в компьютерной игре на экране присутствуют в среднем от 2 до 20 объектов размером примерно 32х32 пикселя, а это достаточно много для работы в реальном времени.
Примечание
Как вы знаете, я привожу в этой книге некоторые алгоритмы бит-блиттинга. Пожалуйста, не используйте их в ваших компьютерных играх! Они написаны исключительно в демонстрационных целях. Вы должны для каждого конкретного случая найти свой вариант.
Рассмотрим еще один важный аспект бит-блиттинга объектов, который вы сможете с выгодой использовать. Если вы твердо знаете, что размер ваших объектов будет фиксированным (например, 16х16 или 32х32), то можете написать отдельный алгоритм бит-блиттинга, специально оптимизированный для каждого из этих размеров. Помните: не существует общих правил быстрого написания компьютерных игр. Нет уже готовых алгоритмов и написанных кем-то библиотек. Вы должны будете использовать по максимуму весь свой творческий потенциал. Только, не пугайтесь. Даже самая замысловатая игра использует алгебру и геометрию не более чем на уровне высшей школы. Это, конечно, не значит, что вы не можете использовать сверхсложные алгоритмы и забираться в дебри, высшей математики. Однако в результате может оказаться, что двенадцатилетний подросток найдет путь в 100 раз более быстрый и эффективный только потому, что он не такой умный как вы!
Установка обработчика прерывания
Регистрация (или установка) подпрограммы обработки прерываний заключается том, что мы помещаем ее начальный адрес в соответствующее поле таблицы векторов прерываний. Это можно сделать двумя способами:
Мы можем просто изменить адрес прерывания таким образом, чтобы он Указывал на наш собственный обработчик и так все и оставить. Однако при этом, старый обработчик прерывания никогда больше вызываться не будет.
Кроме того, а что произойдет, если прерывание будет сгенерировано именно в тот момент, когда мы будем заниматься заменой адреса в таблице векторов? Р-р-раз и «зависли» — вот что случится! Именно поэтому пусть это действие за вас выполнит DOS при помощи функции _dos_setvect(). Для сохранения старого вектора прерывания следует использовать функцию _dos_getvect(). Обе функции гарантировано изменяют вектора без ущерба для операционной системы;
Очень часто нам требуется расширить функциональные возможности системы без полной замены уже работающих функций. Тогда мы используем цепочку прерываний. Под цепочкой прерывания понимается следующее: мы запоминаем старый адрес процедуры обслуживания прерывания и заменяем его на собственный, затем, по окончании нашей процедуры обработки прерывания, передаем управление старому обработчику. Таким образом, нам удается сохранить чужие обработчики прерываний или резидентные программы - если, конечно, нам это надо!
Взгляните на рисунок 12.3, на котором представлены два способа установки обработчиков прерываний.
Листинг 12.1 демонстрирует установку нашей процедуры обслуживания прерывания timer () (которая по-прежнему ничего не делает).
Листинг 12.1. Установка процедуры обслуживания прерывания.
void (_interrupt _far old_isr) (); // Указатель для сохранения
// вектора старого
// обработчика прерываний
// Сохранить вектор старого обработчика прерывания таймера
old_isr = _dos_getvect( 0х1C);
// Установить новую процедуру обработки прерывания
_dos_setvect(0x1C, Timer);
Ну как, трудно? Да вовсе нет! Для восстановления старого обработчика нам потребуется сделать только:
_dos_setvect(0x1C, old_isr);
Вот и все. Теперь, для примера, заставим наш обработчик прерывания делать что-нибудь полезное, например, обновлять значения глобальной переменной. В Листинге 12.1 показан текст программы SPY.С, которая представляет собой бесконечный цикл, печатающий значение переменной. Единственный способ изменить значение этой переменной - присвоить ей значение из другой программы, например, из обработчика прерывания.
Листинг 12.2. Шпионим за часами (SPY.C)._________________
// ВКЛЮЧАЕМЫЕ ФАЙЛЫ ///////////////////////////////////////////////
#include
#include
#nclude
#include
#include
#include
// ОПРЕДЕЛЕНИЯ//////////////////////////////////
#define TIME_KEEPER_INT 0x1C
// ГЛОБАЛЬНЫЕ
ПЕРЕМЕННЫЕ
///////////////////////////////
void (_interrupt _far *old_Isr)();
// хранит старый обработчик прерывания
long time=0;
// функции ////////////////////////////////////
void _interrupt _far Timer()
{
// Увеличивает глобальную переменную.
// Еще раз отметим, что мы можем это делать, так как при входе
// в процедуру обработки прерывания регистр DS
указывает на сегмент
// глобальных данных нашей программы,
time++;
} // конец Timer
// ОСНОВНАЯ ПРОГРАММА ////////////////////////////////
void main(void)
{
// установка процедуры обработки прерывания
Old_Isr = _dos_getvect(TIME_KEEPER_INT);
_dos_setvect(TIME_KEEPER_INT, Timer) ;
// ожидание нажатия клавиши пользователем
while(!kbhit())
{
// вывод переменной. Примечание: сама по себе функция main
// значение этой переменной не изменяет...
_settextposition(0,0);
printf("\nThe timer reads:%ld ",time);
} // конец while
// восстановление старого обработчика прерывания
_dos_setvect(TIME_KEEPER_INT, Old_Isr) ;
} // конец функции main
Запустив программу, приведенную в Листинге 12.2, вы увидите, что приращение счетчика происходит очень быстро. Фактически, значение этой переменной увеличивается каждую 1/18.2 секунды — так настроены по умолчанию внутренние системные часы. (Не переживайте, очень скоро мы научимся управлять и ими).Главное, что сама программа не увеличивает значенй переменной time. Этим занимается исключительно подпрограмма обслужива ния прерывания.
Итак, мы создали процедуру обслуживания прерывания, установили ее и убедились в том, что она работает - и все это мы сделали, написав лишь несколько строк кода. Восхитительно, не правда ли? Более сложными обработчиками прерываний мы займемся чуть позже. Сейчас же давайте сменим тему и поговорим об игровом цикле.
Установка прерывания
Однажды пройдя через все эти фокусы, чтобы установить простое прерывание, можем окончательно инсталлировать наш собственный вектор ISR, зависящий от СОМ-порта. Запомните, что порты 3 и 4 используют те же самые прерывания, что и порты 1 и 2 соответственно.
Таблица 14.4. Векторы прерывания последовательного порта.
Установка видеорежимов
В играх есть немало технических хитростей: работа со звуком, «искусственный интеллект» и многое другое. Но перед тем как начать этим заниматься, давайте попробуем инициализировать наш дисплей.
Конечно, мы сразу можем набрать гору документации с подробным описанием устройства дисплея, регистров и установок, но все это весьма опасно, и вот почему. То, что будет работать на одной видеокарте, может оказаться абсолютно неработоспособным на другой. Таким образом, чтобы избежать возможной несовместимости, для установки видеорежима мы будем использовать базовую систему ввода/вывода (BIOS).
Можно смело сказать, что основная графическая мощь ПК сосредоточена в прерывании с номером 10h. Использование этого прерывания весьма просто — необходимо правильно установить нужные регистры процессора в зависимости от выполняемой функции. В этой книге мы будем пользоваться режимом 13h (это графический режим с разрешением 320х200 точек, при 256 цветах). Теперь нам нужно найти, как перенести компьютер в этот режим. Для этого давайте напишем программу на ассемблере для установки режима 13h и программу на Си для проверки. Соответствующие фрагменты показаны в Листингах 2.8 и 2.9.
Листинг 2.8. Ассемблерная процедура, устанавливающая видеорежим (SETMODEA.ASM).
.MODEL MEDIUM, С
;модель памяти - MEDIUM, соглашения языка Си
.CODE ;начало кодового сегмента
PUBLIC Set_Mode ;объявляем функцию как общедоступную
Set_Mode PROC FAR С vmode:WORD ;функция получает один параметр
mov АН,0 ;функция 0 прерывания 10h - установка режима
mov AL,BYTE PTR vmode ;номер режима, который вы хотите установить
int 10h ; используем BIOS для установки режима
ret ; возврат из процедуры
Set_Mode ENDP ; конец процедуры
END ;конец кодового сегмента
Листинг 2.9. Си-функция, тестирующая видеорежим (SETMOPEC.C).
#include
#define VGA256 0х13
#define TEXT_MODE 0х03
extern Set_Mode(int mode);
void main(void)
{
// устанавливаем режим 320х200 точек, 256 цветов
Set_Mode(VGA256);
// ждем нажатия любой клавиши
while (kbhit()) {}
// возвращаем компьютер в текстовый режим
Set_Mode(TEXT_MODE);
} // конец функции main
Теперь если вы наберете и запустите эти программы, то, скорее всего, увидите пустой экран. Правда, в данном случае это не означает «зависание» компьютера, а свидетельствует о том, что VGA-карта переключилась в режим 13h. Стоит только нажать любую клавишу, и вы вновь окажетесь в привычном текстовом режиме 80х25. Конечно, можно было бы использовать функцию _setvideomode() из графической библиотеки Microsoft С, но наша функция работает в сотни раз быстрее.
Теперь, когда мы научились переключать экран в графический режим, неплохо бы попробовать его очистить.
Установки и статус UART
Установки и статус UART управляются через набор внутренних регистров доступных как порты ввода/вывода, адреса которых начинаются от некоторого базового адреса. Базовый адрес определяется номером последовательного порта через который вы хотите связаться. Рассмотрим таблицу 14.1, в которой указаны базовые адреса управляющих регистров UART.
Таблица 14.1. Базовые адреса управляющего регистра UART.
Последовательный порт Базовый адрес порта
СОМ1 3F8h
COM2 2F8h
COM3 3E8h
COM4 2E8h
Как видите, если мы хотим играть через последовательный порт СОМ1, нам необходимо использовать порт 3F8h в качестве базового адреса ввода/вывода. Каждый порт имеет девять регистров, в которые можно писать или из которых можно считывать информацию в зависимости от их типа. Следовательно, для доступа к регистру 1 порта СОМ1 необходимо использовать адрес ввода/вывода 3F8h+1, то есть 3F9n.
Теперь мы знаем, где расположены регистры. А что каждый из них делает?
Это регистр поддержки передачи, куда помещается следующий символ для передачи. Если это одиночный байт и вы используете схему передачи, имеющую менее 8 бит, то данные игнорируются, и не передаются вообще.
Регистр 0 также выполняет функции буферного регистра приема. В зависимости от того, пишете вы в него или читаете из этого регистра, буфер передает или принимает символы с другого компьютера соответственно. В любом случае, при чтении из этого регистра он содержит последний переданный ему символ.
Этот регистр используется, чтобы задействовать тот тип прерываний, который может сгенерировать UART. Он доступен как для чтения, так и для записи. После установки серийного порта было бы неудобно постоянно опрашивать его, поэтому для получения входных данных лучше написать процедуру обслуживания прерывания (ISR), которая будет вызываться каждый раз при получении символа.
Этот регистр позволяет нам сообщить UART'y, какие именно события Должны вызывать прерывание. Для нас представляет интерес только прерывание RxRDY, которое генерируется при получении символа UART'ом.
Регистр идентификации прерывания используется для определения причины по которой UART выдал прерывание. Это может показаться избыточным однако если вы предварительно установили UART для получения прерывания по двум или более различным событиям, то, поскольку этот регистр определяет тип произошедшего прерывания, он поможет вам выяснить, что именно произошло.
Регистр управления линией используется для изменения некоторых характеристик последовательного порта, таких как количество передаваемых битов данных, тип четности. Этот регистр также выполняет функции управления загрузкой старшего и младшего байтов делителя, задающего скорость передачи и имеющего WORD. Этот регистр также доступен и для записи, и для чтения.
Данный регистр оказывает влияние на некоторые выходные данные линий управления модема. Нас больше всего в нем интересует бит GP02. Когда он установлен, появляется возможность прихода прерываний.
Регистр состояния линии используется, чтобы обрисовать состояние коммуникационного порта. В этом регистре нас интересует пятый бит (ТВЕ), который используется для определения возможности продолжения передачи символов (THR).
Регистр состояния модема используется, чтобы показать состояние линий управления модемом. Для наших целей этот регистр едва ли понадобится. Однако вы могли бы найти применение индикатору звонка (RI). Вы можете написать программу, которая будет перехватывать звонок и когда вызывается ваш номер, например, сообщать об этом соответствующей надписью на экране и звуком.
Регистр 7: Регистр временного заполнения (Scratch-Pad Register)
Не используется
Регистр 8: Менее значимый ключ делителя скорости передачи (Baud-Rate Divisor Latch Least-Significant Byte - DLL)
Предназначен для хранения младшего байта делителя, используемого при вычислении действительной скорости передачи через порт.Окончательная скорость вычисляется так: берут младший и старший банты и используют их как делитель числа 115200. В результате получится скорость передачи. Этот регистр доступен через регистр 0 при установленном 7-м бите (DLAB) регистра З (LCR).
Регистр 9: Регистр более значимого байта ключа делителя скорости пepeдaчи,(Baud-Rate Divisor Latch Most-Significant Byte-DLM)
Этот регистр используется для поддержки старшего байта делителя, использумого для вычисления действительной скорости передачи через последовательный порт. Окончательная скорость передачи вычисляется следующим образом: берут старший и младший байты и используют их как делитель, на который нужно разделить число 115200. Это дает скорость передачи. Данный регистр доступен через регистр 1 при установленном 7-м бите (DLAB) регистра 3 (LCR).
Устранение эффекта сдвига кадра
На медленных машинах или на машинах с медленными видеокартами можно заметить некий сдвиг изображения, как будто оно копируется на экран. Из-за эффекта сдвига изображение выглядит как бы разорванным. Этот интересный но нежелательный эффект появляется оттого, что адаптер сканирует видеобуфер и рисует изображение на дисплее примерно 60 раз в секунду. Этот процесс называется регенерацией экрана. Если программа в момент начала регенерации дисплея находится в процессе рисования кадра, вы заметите эффект сдвига изображения.
К счастью, существуют методы проверки статуса регенерации экрана. На VGA-карте есть регистр, сообщающий, регенерируется ли экран в настоящее время- Все, что требуется для устранения эффекта сдвига кадра, это подождать, пока регенерация экрана завершится. Затем можно начать рисовать изображение.
В Листинге 17.7 содержится фрагмент программы, ожидающей завершения цикла регенерации экрана. Это дает вам примерно 1/60 секунды, чтобы нарисовать следующий кадр. Данный фрагмент можно поместить непосредственно перед функцией, перемещающей кадр из системной памяти в видеобуфер. Выполняйте такую проверку каждый раз перед копированием буфера на экран. Только на очень быстрых машинах или при использовании небольшого окна вывода, одной шестидесятой секунды будет достаточно для изображения нескольких планов и их копирования на экран. Это главный недостаток режима 13h. Единственная альтернатива проверке на регенерацию экрана — это использование видеорежимов, поддерживающих несколько видеостраниц, и переключение между ними.
Листинг 17.7. Проверка вертикальной трассировки.
asm mov dx,0x3da
NoRetrace:
asm in al,dx
asm and al,8
аsm jz NoRetrace // ждать, пока трассировка завершится
Retrace:
asm in al,dx
asm and al,8
asm jnz Retrace // ждать начала трассировки
Программа в этой главе не выполняет проверку вертикальной трассировки. Это было сделано для того, чтобы свести к минимуму использование ассемблера. Настоятельно рекомендую использовать этот фрагмент во всех программах вывода графики.
Устройство и архитектура звуковой карты Sound Blaster
Sound Blaster — сложная звуковая карта с большими возможностями. Управлять ею не так уж и просто, и мы наверняка не сможем рассмотреть все ее впечатляющие способности в одном-единственном разделе. Поэтому мы приведем здесь только основные характеристики современных моделей Sound Blaster и кратко остановимся на их функциональных различиях.
На данный момент существует четыре различных модификации Sound Blaster. Их характеристики приведены в таблице 9.1.
Sound Blaster может создавать два типа звуков:
§ Синтезированный звук;
§ Оцифрованный звук.
Синтезированный звук создается искусственно с помощью электронной аналоговой или цифровой аппаратуры. В Sound Blaster применяется современный подход к синтезу звука — метод цифровой частотной модуляции (FM synthesis). В этом методе для создания звука используется та самая частотная модуляция, которую используют и музыкальные радиостанции, работающие в УКВ-диапазоне. Мы поговорим об этом подробнее позже в этой главе.
Sound Blaster также имеет процессор цифровых сигналов (digital signal processor, DSP), который помогает в синтезе и воспроизведении MIDI-музыки. По-существу, MIDI (musical instrument digital interface, цифровой интерфейс электромузыкальных инструментов) — это стандарт для оцифровки голоса и инструментальной музыки таким образом, чтобы они могли быть воспроизведены с помощью компьютера или такого синтезатора, как Yamaha.
Кроме синтеза звука Sound Blaster позволяет оцифровывать; а затем воспроизводить такие звуковые фрагменты, как речь или различные эффекты. Это очень полезное свойство, так как некоторые звуки очень сложно или даже невозможно создать только с помощью частотного синтезатора и DSP. В наших играх мы как раз и будем использовать оцифрованные звуковые эффекты вместе с MIDI-музыкой.
В следующем разделе мы рассмотрим как Sound Blaster работает с оцифрованным звуком.
В каком формате должны быть представлены данные MIDI?
В случае настоящего MIDI-устройства, такого как MPU401 или SoundCanvas, ему будут передаваться данные MIDI без каких-либо изменений (исключая сообщения SysEx).
В случае Adiib или Sound Blaster, MIDPAK будет эмулировать устройство MIDI и эта эмуляция имеет определенные ограничения;
§
Каналы 2-9 используются для мелодических инструментов;
§ Канал 10 используется для ударных инструментов;
§ Загружаемые алгоритмы должны быть в формате обобщенного MIDI.
Вектор Номер Адресная функция
0х0В 0x002C-0x002F RS-232 порт 1
0х0С 0х0030-0х0033 RS-232 порт 2
Все что нам нужно сделать для установки нового ISR, это использовать функцию Си _dos_getvect(), чтобы запомнить прежнее значение вектора, и _dos_setvect(), чтобы инсталлировать наш собственный ISR на место старого. Далее, с приходом прерывания (то есть когда получен символ), будет вызываться наша процедура. Звучит это великолепно, но что она будет делать?
Наш ISR должен выполнять только одну задачу — получить символ из регистра приемного буфера (RBR) и поместить его в программный буфер. Чтобы основная программа могла брать поступающие символы по мере надобности, мы должны буферизировать ввод. С этой мыслью создадим буфер с перезаписью и установим его размер равным 128 байтам, хотя, вообще-то, его длина может быть любой.
Алгоритм буферизации работает так. Полученный из RBR следующие символ помещается в буфер в текущую позицию. Далее текущий индекс буфера инкрементируется. Когда позиция записи в буфере доходит до конца, она перемещается к началу. Как вы понимаете, при этом данные, которые были записаны ранее, окажутся перекрыты. Надеюсь, что до того, как это произойдет основная программа успеет прочитать символы из буфера и обработать полученные данные. Рисунок 14.3 поясняет принцип работы буфера с перезаписью.
Мы должны обсудить еще одну тонкость, прежде чем закончим разговор об ISR. Непосредственно перед выходом из процедуры обработки прерывания необходимо сообщить PIC'y о ее завершении. Для этого в конец процедуры нужно вставить команду записи в порт 20h значения 20h. Если этого не сделать, произойдет сбой системы. Но это — между прочим, ибо пока вы используете функции Си, об этом не стоит беспокоиться. Вот если бы вы решили писать программы исключительно на ассемблере, то вопрос правильного завершения прерываний оказался бы весьма актуален и мы обсудили бы его более подробно. Но давайте пока остановимся на Си.
Листинг 14.1 показывает операции с ISR.
Листинг 14.1. Операция ISR.
void _interrupt _far Serial_Isr(void)
{
// Это процедура обработки прерывания СОМ-порта. Она очень проста.
// При вызове она читает полученный символ из регистра 0 порта
//и помещает его в буфер программы. Примечание: язык Си сам
// заботится о сохранении регистров и восстановлении состояния
// запрещаем работу всех других функций
//во избежание изменения буфера
serial_lock = 1;
// записываем символ в следующую позицию буфера
ser_ch = _inp(open_port + SER_RBF);
// устанавливаем новую текущую позицию буфера
if (++ser_end > SERIAL_BUFF_SIZE-1) ser_end = 0;
// помещаем символ в буфер
ser_buffer[ser_end] = ser_ch;
++char_ready;
/ / восстанавливаем состояние контроллера прерываний
_outp(PIC_ICR,0x20);
// разрешаем работу с буфером
serial_lock = 0;
} // конец функции
Программа из Листинга 14.1 выполняет все то, о чем мы говорили. Однако стоит обратить внимание на одну маленькую деталь. В программу включена переменная serial_lock, которая оберегает основную программу от конфликт тов связанных с обращением к буферу, пока ISR обновляет его. Такой прием называется «блокировкой» или «семафором». В DOS'e подобной проблемы никогда не возникает по ряду причин, о которых говорить слишком долго. Необходимость регулирования доступа к общим данным возникает только для полностью многозадачных систем. Тем не менее, введение «семафоров» - хорошая практика, даже если на данном этапе такая техника и не нужна. Все, мы почти у цели!
Вероятностные автоматы
Наверное, вы уже поняли, как вероятность и случайные числа могут быть использованы для выбора направлений и состояний. Мы научились использовать случайные последовательности для конструирования «характера» персонажей. Я имею в виду, что «Муха» в нашем предыдущем примере могла самостоятельно выбирать различные состояния, основываясь на окружающей обстановке. Если несколько изменить метод выбора состояний, основанны на генерации случайных чисел (то есть, создать условия, при которых вход в опредёленное состояние стал бы легче или тяжелее), то, в конечном счете, нам удалось бы изменить "характер" «Мухи».
Скажем, нам захотелось иметь в игре две «мухи». Если одну и ту же программу использовать для создания траектории движения каждой «мухи», они бы действовали одинаково. Во многих случаях большего и не требуется. Однако гораздо интересней иметь много «мух» с небольшими различиями в поведении. Это можно было бы реализовать изменением диапазона случайных чисел-во всех строках программы, где выбираются состояния «мухи». Но такой подход будет очень грубым. Мы пойдем другим путем — путем создания общего метода управления характером персонажей, основанного на вероятности.
В искусственном интеллекте «индивидуальность» означает возможность существ по-разному выполнять определенные действия при одних и тех же обстоятельствах. Например, у меня есть несколько достаточно активных друзей, которые захотели бы слегка проучить плута, попытавшегося их надуть. Но у меня также есть друзья, которые более спокойны и предпочитают сначала думать, а потом действовать. Скорее всего, мошеннику удалось бы как-то с ними договориться. То, что мы видим на данном примере и является «индивидуальностью». Каким именно способом это будет достигнуто, не принципиально, важен конечный результат.
В видеоиграх мы могли бы иметь несколько противников, которые постоянно преследуют нас, пока другие в это время неподвижны и стреляют. Третьи трусливы и предпочитают убегать, а не сражаться. Анализируя ситуацию, мы видим, что имеется все тот же набор состояний, но вероятности перехода в них различны для каждого создания.
Попытаемся вначале проанализировать проблему. Нам необходимо определить вид объекта, который зависит от направления взгляда игрока и траектории объекта или направления его движения. Как мы уже говорили, луч зрения игрока можно зафиксировать и считать, что он всегда перпендикулярен экрану. Тогда нам нужно будет побеспокоиться только о векторе траектории объекта, выводимого на экран. На рисунке 8.7 изображена взаимосвязь между вектором направления взгляда игрока и некоторой траекторией передвижения объекта.
Теперь мы должны сделать вот что: возьмем игрушечную машинку или что-нибудь подобное и будем передвигать ее перед собой (шум мотора имитировать при этом не обязательно, можно все делать тихо). Проделав это, вы быстро придете к выводу, что рисуемое на экране изображение космического корабля, движущегося прямолинейно, практически одинаково для всех параллельных траекторий независимо от местоположения объекта. Конечно, это справедливо только частично, зато мы получили хорошую отправную точку для нашего первого алгоритма выбора правильного кадра.
Что же мы должны сделать:
§ Вычислить угол между траекторией движения объекта и лучом зрения игрока (который всегда направлен прямо в экран);
§ Разделить полученный угол на квадранты. Затем на основании полученного индекса выбрать наиболее подходящее изображение среди предварительно подготовленных оцифровкой фотографий модели или нарисованных в графическом редакторе. (Более подробно это обсуждается в разделе «Оцифровка объектов и моделирование».)
§ Вывести на экран подходящий кадр, используя аксонометрическую проекцию и масштабируя объект до необходимого размера.
В результате на экране получается реалистичная картина.
Каким же образом находится угол между траекторией объекта и лучом зрения наблюдателя? Ответ может быть получен с помощью скалярного произведения векторов.
Мы знаем, что угол между двумя векторами можно найти с помощью скалярного произведения векторов, как это показано на рисунке 8.8.
Формула 8.4. Вычисление угла между наблюдателем и объектом.
Если мы зададим вектор направления взгляда, как V, а вектор скорости, как О, тогда угол между ними можно будет найти по следующей формуле:
Пусть V = (vx,vy,vz) и О = (ox,oy,oz), тогда
Если бы мы хотели сформулировать это действие словами, то могли бы сказать так: «Угол между V и О равен арккосинусу скалярного произведения этих векторов, разделенного на произведение длин векторов».
Угол между V и О, рассчитанный по этой формуле, имеет одну особенность: он всегда внутренний, то есть больше 0, но меньше 180 градусов. Следовательно, один и тот же результат, полученный по этой формуле, может соответствовать двум разным углам. Это происходит потому, что скалярное произведение не дает информации о направлении вектора (или о направлении, в котором вы отсчитываете положительный угол). Другими словами, эта формула всегда выдает наименьший из углов между двумя векторами. Если вы будете помнить об этом, то такое поведение данной формулы не будет большой проблемой. (Это напоминает бутерброд, который всегда падает маслом вниз. Если вы не знаете об этом, то такой результат может свести вас с ума. А кто предупрежден, тот вооружен.)
Рисунок 8.9 иллюстрирует указанную проблему графически. На этом рисунке показан вектор направления взгляда, три возможных положения вектора траектории и полученный в результате расчетов по формуле 8.4 угол.
Кстати, формулу 8.4 можно значительно упростить, вспомнив, что нас интересует только плоскость X-Z, так как луч зрения всегда перпендикулярен плоскости просмотра.
Но как же, в конце концов, определить действительный угол? Конечно, вы Могли бы воспользоваться еще и векторным произведением, чтобы решить, корректен ли угол, полученный в результате расчетов по формуле 8.4 или необходимо увеличить его еще на 180 градусов. Однако я слишком не люблю математику (возможно, именно поэтому я и доктор математических наук) и предпочитаю вместо грубой силы использовать тонкую интуицию.
Если мы сообразим, что вектор траектории объекта имеет ту же исходную точку, что и вектор направления взгляда, а затем проверим, в какой из полуплоскостей относительно луча зрения расположен Х-компонент вектора траектории, то мы сможем определить, больше или меньше 180° искомый угол. Это наглядно изображено на рисунке 8.10.
Применяя метод проверки Х-компонента, мы можем написать простую функцию, которая вначале рассчитывает угол, используя скалярное произведение, а затем проверяет, находится ли координата Х справа (положительная) или слева (отрицательная) от вектора направления взгляда. Если координата Х положительная, мы вычитаем угол, полученный с помощью формулы 8.4 из 360 градусов (это все равно, что прибавить 180). Затем мы можем взять рассчитанный угол и разбить его на 12 квадрантов (либо взять его модуль по основанию 12). Полученное число затем можно использовать как индекс для нахождения кадров спрайта. (Конечно, кадры должны быть расположены в правильном порядке, то есть кадрам, полученным при вращении объекта против часовой стрелки с шагом в 30 градусов, должны соответствовать индексы от 0 до 11. При этом нулевой индекс должен указывать на кадр объекта, повернутого тыльной стороной к наблюдателю.)
Если значение координаты Х отрицательное, происходит то же самое за исключением того, что будет использован другой банк изображений, и оперировать потребуется с абсолютным значением X.
Кадры, которые я создал для демонстрации этого алгоритма, расположены в файле VRYENTXT.PCX. Они расположены слева направо и сверху вниз. Каждая картинка содержит изображение, повернутое на 30° против часовой стрелки, а в исходной позиции нос корабля направлен прямо в экран (или, с точки зрения игрока, корабль обращен к нему тыльной стороной). Этот же файл мы использовали и в предыдущем примере.
Демонстрационная программа будет использовать рассчитываемые углы для выбора кадров. Но мы же не можем поместить корабль просто в пустоту. Это будет скучно! Нам надо добавить что-нибудь для оживления картинки.Я предлагаю создать трехмерное звездное небо. Под трехмерностью я здесь понимаю то, что звезды будут перемещаться к вам или от вас, а не влево или вправо, как это мы делали раньше, Надо отметить, что космический корабль, летящий в звездном пространстве, выглядит превосходно. Однако следует еще поговорить о том, как же создается такое трехмерное звездное небо.
Вертикальный обратный ход луча
Образ, рисуемый на экране ЭЛТ (электронно-лучевой трубки) и управляемый картой VGA, образуется в результате взаимодействия следующих факторов:
§ Луч электронов движется, по экрану слева направо и сверху вниз, рисуя картинку;
§ Когда он достигает нижней границы, он вновь возвращается вверх и все начинается сначала.
Рисунок 5.16 показывает это.
§ Чтобы вернуться в исходную позицию, лучу требуется примерно 1/60 секунды. Это идеальное время для обновления видеобуфера. В течение этого периода видеобуфер недоступен VGA-карте. Таким образом, 1/60 секунды — это аппаратно-зависимый параметр.
В седьмой главе, «Продвинутая битовая графика и специальные эффекты», мы узнаем, как «синхронизировать» наши игры с этим сигналом и создать чистые, свободные от мерцания изображения.
Video for Windows
Когда все ваши технические проблемы решены и вы получили приличное видеоизображение вашего макета, настало время использовать компьютера программное обеспечение (или, скорее, я бы сказал, его отсутствие) для того, чтобы перевести его в цифровую последовательность. Я сам не люблю понапрасну тратить много денег, и поэтому не буду и вас убеждать приобрести оборудование и программное обеспечение на сотни или тысячи долларов. Следовательно, нам придется использовать то, что поставляется с платой ввода графической информации. Я имею в виду Microsoft Video for Windows, которая, в общем-то, предназначена, скорее, для оцифровки сюжетов, а не для моментальных снимков вроде наших.
Так как почти все платы ввода графической информации продаются вместе с Microsoft Video for Windows, я хочу рассказать, как использовать Video for Windows для обработки образов ваших игровых объектов.
§
При создании каждого кадра используйте опцию single-frame (одиночный кадр);
§ Создав кадр, сохраняйте его в формате BMP-файла, а затем с помощью другой программы преобразуйте его в PCX-формат.
§ При создании кадров с вращающимся объектом расположите под ним круглый транспортир, чтобы точно повернуть объект на нужное количество градусов. Затем, когда вы оцифруете все ваши образы, запишите их в файлы с именами nameххх.bmp, где ххх - угол поворота. Я считаю, что для получения вполне реалистичной картины движения, перед очередным кадром можно поворачивать объект на 30°.
§ Завершив оцифровку всех кадров, вы должны преобразовать их в формат PCX с помощью какого-нибудь графического редактора или специализированной программы. (Большинство графических редакторов для персональных компьютеров поддерживают PCX-файлы в режиме 320х200х256.)
§ После того как все ваши файлы сохранены в формате PCX, нужно тщательно вычистить из них фон и попавшую в кадр платформу.
§ Затем возьмите все кадры и разместите их в одном файле PCX с общей палитрой. Я пришел к выводу, что для получения реалистичного изображения не требуется изготавливать картинки размерами более чем 128х128 пикселей.
Описанная здесь последовательность действий в виде алгоритма представлена на рисунке 8.13.
ВИДЕОИГРЫ. ПЕРВЫЕ ШАГИ...
С чего начать? Хочется так много сказать, что невольно придется посвятить этому несколько страниц. То путешествие, которое мы собираемся предпринять в мир разработки видеоигр, можно смело назвать захватывающим приключением. Создание видеоигр можно сравнить с написанием стихов или рисованием картины. Для этого нужно вдохновение, ведь создатель хочет поделиться с окружающим миром частичкой своего воображения. Один великий скульптор сказал однажды; «Статуя была здесь всегда, Я просто освободил ее из камня». Это высказывание вполне применимо и к видеоиграм.
Компыотер — это просто хранилище битов информации и, устанавливая их в 1 или 0, вы создаете образ. В этом заключается искусство. Я хочу, чтобы вы настроились на созидательную работу. Нам потребуется полное взаимопонимание. В этой главе я расскажу о том, как создаются видеоигры. Вы узнаете вот о чем:
§
Кто пишет видеоигры;
§ Откуда берутся идеи;
§ Фазы создания видеоигры;
§ Что вы узнаете из этой книги.
В следующих главах вы узнаете, как писать игры.
Видимый объем
Как мы узнали в главах, посвященных трехмерной графике, базирующейся на многоугольниках (глава шестая, «Третье измерение» и седьмая, «Улучшенная битовая графика и специальные эффекты»), объекты должны быть отсечены в пределах видимого объема (или усеченной пирамиды просмотра). Это достигается путем определения ребер каждого многоугольника и отсечения их шестью гранями видимого объема. Возникающая при этом проблема состоит в том, что объем просмотра представляет собой трехмерный трапецоид, состоящий из шести интересующих нас плоскостей, так как это показано на рисунке 8.3.
Мы, конечно, не хотим рассчитывать пересечения спрайтов с произвольными плоскостями. Это было бы уж слишком! Но если мы вначале спроецируем каждый спрайт в аксонометрии и рассчитаем их масштаб, то сможем превратить объем просмотра в прямоугольник. Теперь, так как мы выполняем обратную операцию, мы сможем отсечь спрайты куда как более легким образом, чем при использовании уравнений произвольных плоскостей.
Этот прием основывается на том факте, что объем просмотра уже является аксонометрической проекцией. Если мы отсечем края трехмерного объекта? видимым объемом прежде, чем спроецируем этот объект, мы будем обязаны использовать трапецеидальные формы объема просмотра. Однако, если мы вначале спроецируем объект с учетом перспективы, а затем
отсечем его прямоугольными границами видимого объема, то.результат, полученный таким образом, будет полностью совпадать с результатом, достигаемым первым способом. Отсечение же в плоскостях, параллельных плоскости просмотра, сводится к простому условному оператору, в котором проверяется, является ли спрайт слишком далеким или слишком близким по отношению к вам.
В случае видеорежима l3h мы можем отсекать все спрайты по размерам экрана или четырехугольника, границы которого определяют точки (0,0) и (319,199). Отсечение в плоскостях, параллельных плоскости просмотра, осуществляется с помощью простого теста на выполнение условия: если спрайт находится внутри наблюдаемого Z-пространства, то визуализируйте объект, а в противном случае игнорируйте его. Отсечение в этих плоскостях выглядит так просто оттого, что в действительности спрайты — это прямоугольные многогранники, расположенные перпендикулярно линии взгляда, или параллельно плоскости просмотра (вы можете думать, как вам больше нравится, но на самом деле это одно и то же).
Я полагаю, что, поэкспериментировав с отсечением дальней плоскостью, вы сделаете так, чтобы объект размером меньше, чем в один пиксель по любому измерению (после масштабирования) вообще не рисовался. Выбирая ближнюю плоскость отсечения, старайтесь сделать так, чтобы приближающийся объект не выглядел как под микроскопом с многократно увеличенными пикселями.
Вопросы и ответы
В этом разделе приведены ответы на наиболее часто задаваемые вопросы, касающиеся озвучивания игр.
При исполнении MIDI-музыки с помощью пакета программ MIDPAK мне кажется, что часть музыкальной композиции теряется
Проверьте назначение каналов. Эмуляция MIDI на Sound Blaster и других картах происходит на каналах 2-9 для мелодических инструментов и на 10 канале для ударных. Многие программы записывают последовательности MIDI, начиная с канала 1. При эмуляции MIDI пакетом MIDPAK канал 1 игнорируется. Назначения каналов MIDPAK были разработаны для эмуляции Roland МТ-32. Несмотря на ряд усовершенствований для поддержки обобщенного MIDI, каналы все еще ограничены номерами со второго по девятый и десятым.
При исполнении MIDI-музыки с помощью пакета программ MIDPAK, она звучит иначе, чем при использовании моего собственного музыкального контроллера. Почему?
Ваш контроллер использует другие алгоритмы, нежели MIDPAK. Чтобы избежать этого, необходимо обратиться к профессиональному композитору, который сможет скорректировать вашу композицию для исполнения с помощью MIDPAK. Однако эти услуги вряд ли будут бесплатными.
Мне не удается изменить громкость с помощью MIDPAK. Почему?
Уровень громкости в MIDPAK изменяется относительно базового уровня данного канала. MIDPAK не может изменить громкость, если вы не определили базовый уровень для каждого канала в вашем файле MIDI. Вы можете определить базовую громкость для MIDI-канала, используя Контроллер 7.
Могу ли я использовать один и тот же MIDI-файл для всех звуковых карт?
Нет, но вы можете сделать нечто похожее. Во-первых, сделайте запись в стандарте обобщенного MIDI. Затем ее нужно скорректировать для Sound Canvas, MIDI, OPL2/OPL3 и МТ-32. Базовые уровни громкости и качество звучания разных алгоритмов для этих устройств немного различаются. Исходные тексты программы SETM (программа конфигурации MIDPAK) входят в поставку (файл SETUP.ZIP) и, в зависимости от звукового драйвера выбранного пользователем, вы можете копировать различные версии вашей музыки.
Восприятие игры
Человеку, который собрался поиграть в компьютерную игру, хочется чтобы она была интерактивной. В данном случае под словом интерактивная я подразу меваю то, что графическое изображение должно быть четким и быстро сменяться. При этом музыка должна звучать в соответствующем темпе и игра обязана мгновенно реагировать на действия игрока (по крайней мере, с точки зрения играющего). Игроку должно казаться, что все (музыка, графика, звуковые эффекты и т. д.) происходит одновременно. Теперь взглянем на это с точки зрения программиста.
Вообще-то, сделать так, чтобы разные события в игре происходили одновременно, сложно. Персональный компьютер — это не многозадачная система (во всяком случае, не для игр, работающих под управлением DOS). Более того, у персонального компьютера только один процессор. Следовательно, иллюзия реальности или "реального времени" должна создаваться как-то иначе. Этот иной способ опирается исключительно на скорость работы компьютера. Быстродействие компьютера настолько превышает скорость человеческого восприятия, что машина успевает выполнять все операции последовательно, а человеку кажется, что все происходит одновременно.
На самом деле, в компьютерной игре все происходит примерно следующим образом: мы получаем команды пользователя, реагируем на них в соответствии с логикой игры, выводим на экран изображения объектов и озвучиваем происходящие события, затем повторяем все снова и снова. Благодаря тому, что это происходит десятки, если не сотни, раз в секунду, нам удается заставить игрока поверить в существование созданного нами виртуального мира.
Воспроизведение оцифрованного звука
Теперь поговорим о воспроизведении оцифрованных звуков. Я не буду показывать вам, как оцифровывать звуки. Вы сами можете воспользоваться одним из десятков программных пакетов, предназначенных для этой цели. Тем более, что в самой компьютерной игре звук не надо записывать, его надо воспроизводить!
Из этих соображений я снабдил эту главу условно-бесплатной программой Blaster Master. Эта программа работает в среде MS-DOS и позволяет делать с оцифрованным звуком все что угодно. Она может записывать звук в файлы различных форматов и применять к звуку специальные эффекты (эхо, реверберацию, смену тональности и т. д.)
Примечание
Оцифрованный звук, как и любая другая информация, должен иметь определенный формат для хранения данных. На персональных компьютерах наиболее распространены форматы WAV и VOC. Оба из них, кроме собственно звуковых данных, имеют специальные заголовки. Формат WAV был предложен в Windows, а формат VOC является стандартом «де-факто».
Blaster Master способен преобразовывать данные из формата VOC в формат WAV и обратно. Демонстрационные игры, которые мы будем рассматривать в этой книге, используют исключительно формат VOC. Поэтому, прежде чем использовать свои звуковые эффекты с нашими примерами программ, вы должны записать их как VOC-файлы или преобразовать их в этот формат, иначе ничего не выйдет! Мы выбрали этот формат из-за того, что он проще для понимания. Кроме того, WAV-файлы могут быть записаны только с частотой 11,22 или 44КГц, а это приводит к большому расходу памяти.
С чего мы начнем? Неплохой вопрос. Начнем-ка мы с драйвера. Чтобы проигрывать звуки, нам понадобится драйвер CT-VOICE.DRV, поставляемый фирмой Creative Labs. Этот драйвер позволяет нам вызывать функции работы со звуковой картой, точно так же, как мы вызываем системные функции BIOS.
Однако этот драйвер не является расширением BIOS и этим отличается от драйверов джойстика и мыши. Он использует другую технологию, называемую загружаемыми драйверами устройств. При таком подходе, драйвер загружается в память и реализованные в нем функции исполняются посредством передачи управления по определенному смещению относительно начала кода драйвера.
Передача параметров функциям драйвера осуществляется загрузкой их значений в определенные регистры процессора.
(Впервые я столкнулся с этой технологией около семи лет назад. И хотя я уже долгое время занимался программированием, не сразу сообразил, что мне надо самостоятельно загрузить драйвер в память, а затем передать ему управление.)
CT-VOICE.DRV имеет множество команд, и я не буду детально обсуждать каждую из них. Поговорим только о тех, которые потребуются нам для загрузки и воспроизведения оцифрованных звуков. В таблице 9.2 приведены необходи мые нам функции драйвера.
Таблица 9.2. Подмножество функций драйвера CT-VOICE.DRV.
Функция
|
Возвращение параметров в вызывающую функции
Когда Си только начинал создаваться, одним из требований в спецификациях называлась «функциональность» языка. Под функциональностью я понимаю возвращение результата и возможность использования комплексных выражений. Например, рассмотрим следующее выражение на Си:
coeff = Factorial(n)*cos(r)*Scale(z);
Это выражение использует три функции и выполняет требуемые математические операции. Результат сохраняется в переменной coeff. Именно это и делает Си «функциональным» языком.
Конечно, это только одна из многих возможностей Си. Таким образом, если мы хотим эмулировать с помощью ассемблера настоящие Си-функции, мы должны иметь возможность возвратить результат в вызывающую функцию, как это делает Си.
Если вы скомпилируете парочку Си-функций, возвращающих значение, и посмотрите на их ассемблерные листинги, то вскоре заметите, что функции всегда возвращают результат в строго определенном наборе регистров. Если же вы прислушаетесь к моим советам, то я гарантирую, что ваши ассемблерные функции будут, по крайней мере, правильно возвращать результаты.
В зависимости от типа, возвращаемые в Си параметры должны находиться в следующих регистрах:
§
BYTE
возвращается в регистре AL;
§ WORD
возвращается в регистре АХ;
§ DWORD должно возвращаться ,в, паре DX:AX, причем в АХ записывается
§ младшее слово;
§ Указатели типа NEAR должны возвращаться в регистре АХ;
§ Указатели типа FAR Возвращаются в паре DX:AX, причем в DX должен содержаться сегмент, а в АХ - смещение.
Давайте для примера вспомним Листинг 2.2, где мы складывали два целых числа и накапливали результат в АХ. К счастью, это именно тот регистр, в котором целое значение может быть возвращено в Си-программу. Если же встречается другая ситуация, (например, результат находится в регистре СХ). то для корректной передачи результата мы должны перед выходом переместить полученное значение в АХ.
Так... Вроде бы, с директивами и техникой программирования на ассемблере MASM мы покончили. Теперь я предлагаю перейти к более живым примерам. Кстати, для них-то все это и написано.
Вращение объектов
Для того чтобы вращать объект, мы должны повернуть его вокруг одной из координат. В двухмерной графике для этого обычно выбирается ось Z. Пока мы находимся в двухмерном мире, нас не беспокоит третье измерение - мы просто не придаем ему значения.
Если экран — это плоскость X-Y, то ось Z — это перпендикуляр к осям Х и Y. Таким образом, если мы описываем наши объекты относительно двухмерного мира, то у нас появляется возможность вращать их относительно оси Z,
Следующие формулы позволяют вращать произвольную точку (X,Y) относительно оси Z:
new_x = x*cos(angle) - y*sin(angle) new_у = y*cos(angle) + y*sin(angle)
где angle — это угол, на который вы хотите повернуть точку. Кроме этого вам стоит помнить еще пару вещей:
§ Положительные углы имеют эффект вращения по часовой стрелке;
§ Отрицательные углы имеют эффект вращения против часовой стрелки.
Надо также не забывать, что Си использует для своих функций радианы, а не градусы, и все вызовы тригонометрических функций должны передавать в параметрах также радианы. Для того чтобы перевести радианы в градусы, мы должны написать простые макросы.
Deg_To_Rad(deg) {pi*deg/180;}
Rad_To_Deg(rad) {180*rad/pi;}
Другими словами, это значит, что в круге 360 градусов или 2хPi радиан. Теперь нам нужно написать функцию для вращения объекта. Давайте просто используем формулы, не задумываясь о том, как и почему они работают. Функция в Листинге 4.7 делает именно то, что мы хотим.
Листинг 4.7. Вращение объекта.
void Rotate_0bject(object__ptr object, float angle)
{
int index;
float x_new, y_new,cs, sn;
// сначала вычислим синус и косинус угла
сs = cos(angle) ;
sn = sin(angle);
// поворачиваем каждую вершину на угол angle
for (index=0; indexnum_vertices; index++)
{
x_new = object->vertices [index].x*cs-object->vertices[index].y*sn;
y_new = object->vertices [index].y*cs+object->vertices[index].x*sn;
// изменяем исходные координаты.на расчетные
object->vertices[index].x = x_new;
object->vertices[index].y = у_new;
} // конец цикла for
} // конец функции
Думаю, что надо кое-что объяснить. Я вычисляю заранее значения синуса и косинуса для данного угла. Зачем, спросите вы. Ответ прост — для скорости. Ибо заниматься вычислениями тригонометрических функций в процессе работы программы можно позволить себе только, имея математический сопроцессор.
Теперь настало время написать что-нибудь посерьезней. Мне кажется, что надо бы написать что-то более экстравагантное, чем одинокий астероид. Пусть это будут хотя бы несколько астероидов. Давайте сначала спланируем наши дальнейшие действия.
Я хотел бы иметь поле астероидов различных размеров в количестве более 100 штук. И так, чтобы они могли вращаться. Для этого программа должна иметь следующую структуру:
Шаг 1. - Инициировать поле астероидов;
Шаг 2. - Стереть поле астероидов;
Шаг 3. - Трансформировать поле астероидов;
Шаг 4. - Нарисовать поле астероидов;
Шаг 5. - Перейти к Шагу 2, пока пользователь не нажмет на кнопку.
Чтобы сделать это проще, я добавил три новых поля к нашей структуре: одно для угла поворота и два - для скорости (целиком программа представлена в Листинге 4.8).
Листинг 4.8. Программа, которая рисует поле астероидов (FIELD.С).
// ВКЛЮЧАЕМЫЕ ФАЙЛЫ ////////////////////////////////////
#include
#include
#include
// ОПРЕДЕЛЕНИЯ /////////////////////////////////////////
#define NUM_ASTEROIDS 10
#define ERASE 0
#define draw 1
// СТРУКТУРЫ ДАННЫХ ////////////////////////////////////
//определяем структуру "вершина"
typedef struct vertex_typ
{
float x,y; // координаты точки на плоскости
} vertex, *vertex__ptr;
// структура объекта
typedef struct object_typ
{
int num_vertices; // количество вершин объекта
int color; // цвет объекта
float xo,yo; // позиция объекта
float x_velocity; // скорость перемещения по осям Х
float y_velocity; // и y
float scale; // коэффициент масштабирования
float angle; // угол поворота
vertex vertices[16]; // 16 вершин
}object, *object_ptr;
// Глобальные переменные //////////////////////////////
object asteroids[NUM_ASTEROIDS];
// Функции ////////////////////////////////////////////
void Delay(int t)
{
// функция формирует некоторую временную задержку
float x = 1;
while(t—>0)
x=cos(x);
} // конец функции /////////////////////////////////////
void Scale_Object(object_ptr object,float scale)
{
int index;
// для всех вершин масштабируем координаты х и у
for (index = 0; indexnum_vertices; index++)
{
object->vertices[index].x *= scale;
object->vertices[index].y *= scale;
}// end for index
// конец
функции ///////////////////////////////////////////
void Rotate_Object(object_ptr object, float angle)
{
int index;
float x_new, y_new,cs,sn;
// заранее вычислить синус и косинус
cs = cos(angle);
sn = sin(angle);
// поворачиваем каждую вершину на угол angle
for (index=0; indexnum_vertices; index++)
{
x new = object->vertices[index].x * cs - object->vertices[index].y * sn;
у new = object->vertices[index].y * cs + object->vertices[index].x * sn;
object->vertices[index].x = x_new;
object->vertices[index].y = y_new;
} // конец цикла for
} // конец функции //////////////////////////////////////////////////
void Create_Field(void)
{
int index;
// формируем поле астероидов
for (index=0; index
{
// заполнить
все поля
asteroids[index].num_vertices = 6;
asteroids[index].color = 1 + rand() % 14; // всегда
видимый
asteroids[index].xo = 41 + rand() % 599;
asteroids[index].yo = 41 + rand() % 439;
asteroids[index].x_velocity = -10 + rand() % 20;
asteroids[index].y_velocity = -10 + randO % 20;
asteroids[index].scale = (float)(rand() % 30) / 10;
asteroids[index].angle = (float) (-50+(float)(rand()%100))/100;
asteroids[index].vertices [0].x =4.0;
asteroids[index].vertices[0].у = 3.5;
asteroids[index].vertices[l].x=8.5;
asteroids[index].vertices[1].y = -3.0;
asteroids[index].vertices[2].x = 6;
asteroids[index].vertices[2].у = -5;
asteroids[index].vertices[3].x = 2;
asteroids[index].vertices[3].у =—3;
asteroids[index].vertices[4].x = -4;
asteroids[index].vertices[4].у = -6;
asteroids[index].vertices[5].x = -3.5;
asteroids[index].vertices[5].у =5.5;
// теперь масштабируем каждый астероид до нужного размера
Scale_Object((object_ptr)&asteroids [index],
asteroids[index].scale) ;
} // конец цикла for
} // конец функции ///////////////////////////////////////////////////////
void Draw_Asteroids(int erase)
{
int index,vertex;
float xo,yo;
for (index=0; index
{
// рисуем астероид
if (erase==ERASE)
_setcolor(0);
else
_setcolor(asteroids[index].color);
// получить позицию объекта
xo = asteroids[index].xo;
yo = asteroids[index].yo;
// перемещаемся к первой вершине
_moveto((int)(xo+asteroids[index].vertices[0].x),
(int)(yo+asteroids[index],vertices[0].y));
for (vertex=1; vertex
{
_lineto((int)(xo+asteroids[index].vertlces[vertex].x),(int) (yo+asteroids[index].vertices [vertex].y));
} // конец цикла for
по вершинам
// замыкаем
контур
_lineto((int)(xo+asteroids[index].vertices[0].x), (int)(yo+asteroids[index].vertices[0].y));
} // конец цикла for
по астероидам
} // конец
функции ///////////////////////////////////////////////////////////////////////////////////////////
void Translate_Asteroids()
{
int index;
for (index=0; index
// перемещаем
текущий астероид
asteroids[index].xo += asteroids[index].x_velocity;
asteroids[index].yo += asteroids[index].y_velocity;
if (asteroids[index].xo > 600 || asteroids[index].xo < 40)
{
asteroids[index].x_velocity = -asteroids[index].x_velocity;
asteroids[index].xo += asteroids[index].x_velocity;
}
if (asteroids[index].yo > 440 || asteroids[index].yo < 40)
{
asteroids [index].y_velocity = -asteroids[index] .y_velocity;
asteroids[index].yo += asteroids[index].y_velocity;
}
} // конец цикла for
} // конец функции
///////////////////////////////////////////////////////
void Rotate_Asteroids(void)
{
int index;
for (index=0; index | |