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

         

Графический формат PCX


В индустрии компьютерной графики существует так много стандартов, что само слово «стандарт» уже потеряло свой первоначальный смысл. Сегодня существует несколько наиболее известных стандартов: PCX, GIF, RGB, TGA, TIF и многие другие. Нам интересен формат PCX потому, что сегодня он является самым распространенным.

Файл в формате PCX представляет собой закодированное представление изображения. Кодирование необходимо для уменьшения размера файла, поскольку только один образ 320х200 пикселей уже займет 64К памяти. Рисованные объекты обладают большой цветовой избыточностью, и это обстоятельство используется для сжатия изображения.

Примечание

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

Давайте посмотрим на рисунок 5.5 (файл CH19\WARINTR2.PCX на дискете). Это копия экрана из игры Warlock. Как вы можете заметить, там не слишком много цветов. Более того, на нем присутствует множество больших, одинаково окрашенных областей.

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

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


При этом сохраняется



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

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

Файл формата PCX состоит из трех секций:

§                                Первая секция PCX-файла длиной 128 байт содержит различную служеб­ную информацию;

§                                Вторая секция — это данные сжатого образа, которые могут оказаться любой длины;

§                                Третья секция размером в 768 байт содержит цветовую палитру, если она есть. В нашем случае она будет присутствовать, поскольку мы используем 256-цветный режим 13h. Эти 768 байт хранят значения RGB от 0 до 255.

Суммируя вышесказанное, можно нарисовать структуру PCX-файла (рис. 5.7).

Получение информации из заголовка несложно: достаточно прочитать первые 128 байт и отформатировать их в соответствии со структурой, представленной в Листинге 5-9.





Листинг 5.9. Структура заголовка PCX-файла.

typedef struct pcx_header_typ

{

char manufacturer; // всегда 10

char version;       // 0 - версия 2.5 Paintbrush

  // 2 - версия 2.8 с палитрой

  // 3 - версия 2.8 без палитры

  // 5 - версия 3.0 или старше

char encoding;      // всегда 1 - RLE кодирование

char bits_per_pixel;// количество бит на пиксель



  // для нашего случая – 8

int x,y; // координаты верхнего левого угла изображения

int width,height; // размеры изображения

int horz_res;      // количество пикселей по горизонтали

int vert_res;      // количество пикселей по вертикали

char ega_palette[48]; // EGA-палитра. Ее можно игнорировать,

char reserved;         // ничего значимого

char num_color_planes; // количество цветовых плоскостей

                       //в изображении

int bytes_per_line;    // количество байт на одну строку

int palette_type;      // не беспокойтесь об этом

char padding[58];      // ссылка на палитру в конце файла

} pcx_header, *pcx_header_ptr;

Последнюю часть PCX-файла также довольно легко обработать:

§          Необходимо установить указатель на конец файла;

§          Передвинуться вверх на 768 байт;

§          Прочитать 768 байт как палитру.

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

§          Если код прочитанного байта принадлежит множеству 192...255, то мы вычитаем из него 192 и используем полученный результат, как количество повторений следующего байта;

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

Если вы достаточно внимательны, то можете спросить: «А как же быть с пикселями, имеющими значения от 192 до 255? Интерпретируются ли они как RLE-цепочки?» Да, и гениальное решение этого вопроса состоит в том, что такие значения кодируются не одним, а двумя байтами.


Например, если требуется поместить в файл значение 200, то сначала нужно записать число 193 (192-1) как количество повторений, а потом — 200. Посмотрим на рисунок 5.8, чтобы увидеть пример декомпрессии.

Теперь настало время написать программу, реализующую чтение файл формата PCX. Она получилась весьма неплохой. Листинг 5.10 даст вам возможность убедиться в этом самостоятельно.


Листинг 5.10. Программа чтения файла формата PCX.

// размеры

экрана

#define SCREEN_WIDTH 320

#define SCREEN_HEIGHT 200

// структура для хранения данных PCX

файла

typedef struct pcx_picture_typ

{

pcx_header header; // заголовок файла (длина 128 байт)

RGB_color palette[256]; // палитра

char far *buffer; // буфер для размещения изображения

// после

декомпрессии

} pcx_picture, *pcx_picture_ptr;

void PCX Load(char *filename,

pcx_picture_ptr image,int enable_palette)

{

// функция загружает данные из PCX-файла в структуру pcx picture

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

// Отдельные элементы изображения выделяются позднее. Также

// загружается палитра и заголовок

FILE *fp;

int num_bytes,index;

long count;

unsigned char data;

char far *temp_buffer;

// открыть

файл

fp = fopen(filename,"rb");

// загрузить

заголовок

temp_buffer = (char far*)image;

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

{

temp_buffer[index] = getc(fp);

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

// загрузить данные и декодировать их в буфере

count=0;

while (count<=SCREEN_WIDTH * SCREEN_HEIGHT}

{

// получить первую часть данных

data = getc(fp);

//это  RLE?

if (data>=192 &&data<=255)

{

// подсчитываем, сколько байт сжато

num_bytes = data-192;

//читаем байт цвета

data = getc(fp);

// повторяем байты в буфере num_bytes раз

while(num_bytes-->0)

{

image->buffer[count++] = data;

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

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

else

{

// помещаем данные в следующую позицию буфера

image->buffer[count++] = data;

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

} // конец чтения байт изображения



// перейти в позицию, не доходя 768 байт от конца файла

fseek(fp,-768L,SEEK_END);

// читаем палитру и загружаем ее в регистры VGA

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

{

// красная

составляющая

image->palette[index].red   = (getc(fp) >> 2);

// зеленая

составляющая

image->palette[index].green = (getc(fp) >> 2);

// синяя

составляющая

image->palette[index].blue = (getc(fp) >> 2);

} // конец

цикла for

fclose(fp);

// если флаг enable_palette установлен, меняем палитру

// на загруженную из файла

if (enable_palette)

{

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

{

Set_Palette_Register(index,

(RGB_color_ptr)&image->palette[index]);

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

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

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

Функция PCX_Load() — это сердце всей программы. Она загружает PCX-файл, декодирует его в буфере и загружает палитру. Каждый PCX-файл имеет свою собственную палитру в конце файла и я думаю, что вы сами можете добавить возможность загрузки повой палитры в таблицу соответствия цветов.

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

§          Открывает PCX-файл;

§          Читает заголовок;

§          Загружает PCX-файл и декомпрессирует все 64000 пикселей;

§          Загружает цветовую палитру.                    

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

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

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

Возникает вопрос: «Как редактировать PCX-файлы в режиме 320х200х 256?» Для этого можно воспользоваться такими условно-бесплатными программами как VGA-Paint или Pro-Paint. Тем не менее, я надеюсь, что самые расторопные читатели уже давно пользуются копией Electronic Art's Deluxe Paint & Animation. Это одна из самых классных программ для рисования на ПК. Она корректно работает с режимом 320х200х256 и имеет множество полезных функций для преобразования и анимации изображения.


ГРАФИЧЕСКОЕ ОФОРМЛЕНИЕ ИГРЫ


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

Среди свободно распространяемых известны такие программы как MVP Paint, NeoPaint, Desktop Paint 256. Они обладают, одним достоинством, увеличивающим объем , их использования: вы можете «попробовать их, прежде чем скушать». Здесь уместно лишний раз подчеркнуть, что даже за условно бесплатные программы все-таки надо платить.

Кроме того, существует огромное количество коммерческих графических программ. Цены на них колеблются от десятков до тысяч долларов. Один из пакетов, имеющих основание высокую цену и необычайно популярных среди разработчиков игр, называется Electronic Arfc's Deluxe Paint II Enchanced. Существует также программа, создающая настоящие фильмы - Deluxe Paint Animator. Весьма популярны программы, которые комбинируют растровую и векторную графику. К ним относятся, например, Corel DRAW и Micrografx Designer. Другим изобретением и мощной графической программой является Fractal Design Painter, который замечательно имитирует традиционное изобразительное искусство. На самом верху находятся программы для создания настоящих мультфильмов типа Autodesk Animator Pro и 3D Studio предлагающие неограниченные возможности в разработке изображений и мультипликации.



Игра Tombstone


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

В этой демонстрации маленький ковбой ходит по городу с различной скоростью. У вас есть все инструменты, чтобы «дать» ему пистолет и "научить" стрелять. В PCX-файле на дискете вы найдете для этого все необходимые картинки. Вся программа за исключением функции Set_Mode() дана в Листин­ге 5.16. Прилинкуйте Set_Mode(), когда будете создавать исполняемый файл.

Листинг 5.16. Tombstone (TOMB.С).

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

ФАЙЛЫ

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

#include <io.h>

#include <conio.h>

#include <stdio.h>

#include <stdlib.h>

#include <dos.h>

#include <bios.h>

#include <fcntl.h>

#include <memory.h>

#include <math.h>

#include <string.h>

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

#define ROM_CHAR_SET_SEG

0xF000 // сегмент описания символов в ПЗУ

#define ROM_CHAR_SET_OFF

0xFA6E // смещение, соответствующее

// описанию первого символа

#define VGA256            0х13

#define TEXT_MODE         0х03

#define PALETTE_MASK         ОхЗC6

#define PALETTE_REGISTER_RD 0x3C7

#define PALETTE_REGISTER_WR 0x3C8

#define PALETTE_DATA        0x3C9

#define SCREEN_WIDTH      (unsigned int)320

#define SCREEN_HEIGHT     (unsigned int)200

#define CHAR_WIDTH        8

#define CHAR_HEIGHT       8

#define SPRITE_MIDTH      24

#define SPRITE_HEIGHT     24

#define MAX_SPRITE_FRAMES 16

#define SPRITE_DEAD       0

#define sprite_alive      1

#define SPRITE_DYING      2                               

// СТРУКТУРЫ

ДАННЫХ ////////////////////////////////////////

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;

typedef struct pcx_header_typ

{ char manufacturer;

char version;

char encoding;

char bits_per_pixel;

int x,y;

int width,height;

int horz_res;

int vert_res;

char ega_palette[46];

char reserved;

char num_color_planes;

int bytes_per_line;

int palette_type;

char padding[58];

} pcx_header, *pcx_header_ptr;

typedef struct pcx_picture_typ

{

pcx_header header;

RGB_color palette[256];

char far *buffer;

} pcx_picture, *pcx_picture_ptr;

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;

// ВНЕШНИЕ ФУНКЦИИ /////////////////////////////////

extern Set_Mode(int mode);

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

void Set_Palette_Register(int index, RGB_color_ptr color);

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

void PCX_Init(pcx_picture *image);

void PCX_Delete(pcx_picture *image);

void PCX_Load(char *filename, pcx_picture_ptr image, int enable_palette) ;

void PCX_Show_Buffer(pcx_picture_ptr image);

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

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

unsigned int  far *video_buffer_w = (int  far *)0xA0000000L;

unsigned char far *rom_char_set   = (char far *)0xF000FA6EL;

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

void Blit_Char(int xc,int yc,char c,int color) {



// эта функция отображает символ на экране

// используя описание

// символов размером 8х8 точек, хранящееся в ПЗУ

int offset,х,у;

unsigned char data;

char far *work_char;

unsigned char bit_mask = 0х80;

// вычисляем смещение описания символа в ПЗУ

work_char = rom_charset + с * CHAR_HEIGHT;

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

offset = (ус << 8} + (ус << 6) + хс;

for (у=0; y<CHAR_HEIGHT; y++)

{

// сбросить битовую маску

bit_mask = 0х80;

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

{ // если бит равен 1, рисуем пиксель

if ((*work_char & bit_mask))

video_buffer[offset+x] = color;

// сдвигаем

маску

bit_mask = (bit_mask>>1);

} // конец отрисовки строки

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

offset      += SCREEN_WIDTH;

work_char++;

} // конец рисования

} // конец

функции

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

void Blit_String(int x,int y,int color, char *string)

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

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

// Для отображения символов вызывается функция blit_char

int index;

for (index=0; string[index]!=0; index++)

{

Blit_Char(x+(index<<3) ,y, string[index],color);

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

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

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

void Delay(int t)

{

float x = 1;

while(t—>0)

x=cos(x);

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

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

void Set_Palette_Register(int index, RGB_color_ptr color) {

// функция устанавливает элемент таблицы цветов задаваемый

// параметром index. Значения компонент цвета задаются полями

// структуры color.

// указываем VGA-карте, что мы будем изменять регистр палитры

_outp(PALETTE_MASK,Oxff);

// указываем номер изменяемого регистра

_outp(PALETTE_REGISTER_WR, index);

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

// один и тот же порт

_outp(PALETTE_DATA,color->red);

_outp(PALETTE_DATA,color->green);

_outp(PALETTE_DATA,color->blue);



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

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

void PCX_Init(pcx_picture_ptr image)

{

// функция выделяет память для загрузки PCX-файла

if ((image->buffer = (char far *)malloc (SCREEN_WIDTH * SCREEN_HEIGHT +1)));

printf("\ncouldn't allocate screen buffer");

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

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

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

{

// функция отображает на экране точку заданного цвета

// вместо умножения используется сдвиг

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

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

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

void PCX_Delete(pcx_picture_ptr image)

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

_ffree (image->buffer);

} // конец

функции

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

void PCX_Load(char *filename, pcx_picture_ptr image, int enable_palette}

{ // функция загружает PCX-файл и заполняет поля структуры

// pcx_picture, включая изображение (после декомпрессии),

// заголовок и палитру

FILE *fp;

int num_bytes,index;

long count;

unsigned char data;

char far *temp_buffer;

// открыть

файл

fp = fopen(filename,"rb");

// загрузить

заголовок

temp_buffer = (char far *)image;

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

{

temp_buffer[index] = getc(fp);

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

// загрузить данные и декодировать их в буфере

count=0;

while(count<=SCREEN_WIDTH * SCREEN_HEIGHT)

{ // получить первую часть данных

data = getc(fp);

// это RLE?

if (data>=192 && data<=255)

{ // сколько байт сжато?

num_bytes = data-192;

// получить значение цвета для сжатых данных

data = getc(fp);

// заполняем буфер полученным цветом

while(num_bytes-->0)

{

image->buffer[count++] = data;

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

} // конец обработки сжатых данных

else

{

// поместить значение цвета в очередную позицию



image->buffer[count++] = data;

} // конец обработки несжатых данных

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

// перейти в позицию, не доходя 768 байт от конца файла

fseek(fp,-768L,SEEK_END) ;

// загрузить

палигру

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

{

// красная

составляющая

image->palette[index].red = (getc(fp) >> 2);

// зеленая

составляющая

image->palette[index].green = (getc(fp) >> 2);

// синяя

составляющая

image->palette[index].blue = (getc(fp) >> 2) ;

} // конец

цикла for

fclose(fp);

// если установлен флаг enable_palette, установить новую палитру

if (enable_palette)

{

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

{

Set_Palette_Register(index,

(RGB_color_ptr)&image->palette[index]);

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

} // конец установки палитры

} // конец

функции

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

void PCX_Show_Buffer (pcx_picture_ptr image)

{ // функция копирует буфер, содержащий изображение из PCX-файла,

// в видеопамять

_fmemcpy(char far *)video_buffer,

 (char far *) image->buffer,SCREEN_WIDTH*SCREEN__HEIGHT);

} // конец

функции

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

void Sprite_Init(sprite_ptr sprite, int x,int y, int ac, int as,int mc,int ms)

{

// функция инициализирует спрайт

int index;

sprite->x            = x;

sprite->y            = у;

sprite->x_old        = x;

sprite->y_old        = у;

sprite->width        = SPRITE_WIDTH;

sprite->height       = SPRITE_HEIGHT;

sprite->anim_clock   = ac;

sprite->anim_speed   = as;

sprite->motion_clock = me;

sprite->motion_speed = ms;

sprite->curr frame   = 0;

sprite->state        = SPRITE_DEAD;

sprite->num_frames   = 0;

sprite->background  = (char far *)malloc (SPRITE_WIDTH * SPRITE_HEIGHT+1);

// устанавливаем все указатели в значение NULL

for (index=0; index<MAX_SPRITE_FRAMES; index++) sprite->frames[index] = NULL;

} // конец

функции

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



void Sprite_Delete(sprite_ptr sprite)

{ // функция освобождает всю связанную со спрайтом память

int index;

_ffree(sprite->background) ;

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

for (index=0; index<MAX_SPRITE_FRAMES; index++) _ffree(sprite->frames(index]);

} // конец

функции

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

void PCX_Grap_Bitmap (pcx_picture_ptr image, sprite_ptr sprite, int sprite_frame, int grab_x, int grab_y)

{

// функция выделяет один кадр из буфера PCX-файла.

// Предполагается, что изображение размером 320х200 пикселей

// в действительности представляет собой массив 12х8 изображений

// каждое размерностью по 24х24 пикселя

int x_off,y_off, x,y, index;

char far *sprite_data;

//вначале выделяем память для размещения спрайта

sprite->frames[sprite_frame] = (char far *)malloc (SPRITE_WIDTH * SPRITE_HEIGHT);

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

sprite_data = sprite->frames[sprite_frame];

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

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

// Помните: в действительности PCX-файл представляет собой массив

// из отдельных элементов размером 24х24 пикселя.

// Индекс (0,0) соответствует левому верхнему изображению,

// (11,7) - правому нижнему.

x_off

= 25 * grab_x + 1;

y_off = 25 * grab_y + 1;

// вычисляем начальный адрес

y_off

= y_off * 320;

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

{

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

{

// берем очередной байт и помещаем его в текущую позицию буфера

sprite_data[y*24 + х] = image->buffer [y_off + х_off + х];

} // конец копирования строки

// переходим к следующей строке y_off+=320;

} // конец копирования

// увеличиваем счетчик кадров sprite->num_frames++;

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

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

void Behind_Sprite(sprite_ptr sprite)

{

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

// куда будет выведен спрайта

char far *work back;

int work_offset=0,offset,y;



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

work_back = sprite->background;

// вычисляем смещение в видеопамяти

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

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

{

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

_fmemcpy((char far *)&work_back[work_offset], (char far *)&video_buffer[offset], SPRITE_WIDTH);

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

offset      += SCREEN_WIDTH;

work_offset += SPRITE_WIDTH;

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

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

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

void Erase_Sprite(sprite_ptr sprite)

{ // функция восстанавливает фон, сохраненный перед выводом спрайта

char far *work_back;

int work_offset=0,offset,y;

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

work_back = sprite->background;

// вычисляем смещение в видеобуфере

offset = (sprite->y_old << 8) + (sprite->y_old << 6} + sprite->x_old;

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

{

// копируем в видеопамять очередную строку буфера

_fmemcpy((char far *)&video_buffer [offset],(char far *)&work_back[work_offset], SPRITE_WIDTH);

// перейти к следующей строке

offset      += SCREEN_WIDTH;

work_offset += SPRITE_WIDTH;

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

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

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

void Draw_Sprite(sprite_ptr sprite)

{

// функция, рисующая спрайт на экране вместо умножения

// используется

сдвиг

char far *work sprite;

int work_offset=0,offset,x,y;

unsigned char data;

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

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

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

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

{

for (x=0; X<SPRITE_WIDTH; X++)

{

// проверяем, не является ли пиксель "прозрачным" (с кодом 0),

// если

нет - рисуем

if ((data=work_sprite[work_offset+x]))

video_buffer[offset+x] = data;

} // конец вывода строки



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

offset      += SCREEN_WIDTH;

work_offset += SPRITE_WIDTH;

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

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

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

void main(void)

{

long index,redraw;                 

RGB_color color;

int frame_dir = 1;

pcx_picture town, cowboys;

sprite cowboy;

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

Set_Mode(VGA256);

// установить глобальные указатели на область памяти экрана

Set_Screen_Pointers();

// загрузить

фон

PCX_Init((pcx_picture_ptr)&town) ;

PCX_Load( "town. pox", (pcx_picture_ptr)&town, 1) ;

PCX_Show_Buffer((pcx_picture_ptr)&town);

PCX_Delete((pcx_picture_ptr)&town);

// вывести на экран заставку игры

Blit_String(128, 24,50, "TOMBSTONE");

// загрузить

образы

PCX_Init((pcx_picture_ptr)&cowboys) ;

PCX_Load("cowboys.pcx", (pcx_picture_ptr) &cowboys,0) ;

// извлечь все образы из PCX-файла

Sprite_Init((sprite_ptr)&cowboy,SPRITE_WIDTH,100,0,7,0,3) ;

PCX_Grap_Bitmap ((pcx_picture_ptr) &cowboys, (sprite_ptr) &cowboy, 0, 0, 0);

PCX Grap_Bitmap( (pcx_picture_ptr) &cowboys,

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

PCX_Grap_Bitmap((pcx_picture_ptr)&cowboys,

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

PCX_Grap_Bitmap ((pcx_picture_ptr)&cowboys,

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

// очистить

память, выделенную

для загрузки PCX-файла PCX_Delete((pcx_picture_ptr)&cowboys);

Behind_Sprite((sprite_ptr)&cowboy);

Draw_Sprite ((sprite_ptr) &cowboy);

// главный

цикл

cowboy.state = SPRITE_ALIVE;

while

(!kbhit()) {

redraw = 0; // используется как флаг необходимости перерисовки

if (cowboy.state==SPRITE_ALIVE)

{

//не пора ли менять кадр?

if (++cowboy.anim_clock > cowboy.anim_speed)

{

// сбрасываем счетчик времени

cowboy.anim_clock

= 0;

if (++cowboy.curr_frame >= cowboy. num_frames)

{

cowboy.curr_frame = 0;

} // конец обработки достижения последнего кадра

redraw=1;



} // конец смены кадра

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

if (++cowboy.motion_clock > cowboy.motion_speed)

{

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

cowboy.motion clock

=0;

// сохранить старую позицию

cowboy.x_old == cowboy.x;

redraw =1;

//передвинуть спрайт

if (++cowboy.x >= SCREEN_WIDTH-2*SPRITE_WIDTH)

{

Erase_Sprite((sprite_ptr)&cowboy);

cowboy.state = SPRITE_DEAD;

redraw         = 0;

} // конец обработки достижения последнего кадра

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

} // конец обработки ситуации "ковбой жив"

else

{ // пытаемся "оживить" ковбоя

if (rand()%100 == 0 )

{

cowboy.state      = SPRITE_ALIVE;

cowboy.x          = SPRITE_WIDTH;

cowboy.curr frame = 0;

cowboy.anim.speed   = 3 + rand()%6;

cowboy.motion_speed = 1 + rand()%3;

cowboy.anim_clock   = 0;

cowboy.motion_clock = 0;

Behind_Sprite{(sprite_ptr)&cowboy);

}                         

} // конец процесса "оживления"

// теперь состояние спрайта изменено

if (redraw)

{

// удалить спрайт в старой позиции

Erase_Sprite((sprite_ptr)&cowboy) ;

// сохранить фон в новой позиции

Behind_Sprite((sprite_ptr)&cowboy) ;

// нарисовать спрайт в новой позиции

Draw_Sprite((sprite_ptr)&cowboy);

// обновить старые координаты

cowboy.x_old = cowboy.x;

cowboy.у_old = cowboy.у;

} // конец перерисовки

Delay(1000);

} // конец главного игрового цикла

for(index=0; index <300000;index++,Plot_Pixel_Fast(rand()%320,

                                          rand()%200,0));

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

Set_Mode(TEXT_MODE);

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


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


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

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

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

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

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

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

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

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

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

§          Оттенение;

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

§          Цикл игры;

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

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




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

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

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





Сюжет игры

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

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

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

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

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

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

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


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

§          DMA#1

§          IRQ #5

§          I/O port-220h

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

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

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

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

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

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

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

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

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

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

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

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

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

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



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

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

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



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

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

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

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

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

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

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

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

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


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

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

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

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

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

{

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

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

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

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

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

сеll_у,

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

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

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

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

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

y_bound,

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

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

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

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

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

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

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

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

xb_save,

yb_save,

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

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

scale;

long  cast=0,

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



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

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

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

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

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

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

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

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

if ( (view_angle-=ANGLE_30) < 0 }

{

view_angle=ANGLE_360 + view angle;

}

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

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

{

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

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

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

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

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

{

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

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

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

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

y_delta =- CELL_Y_SIZE;

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

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

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

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

next_у_cell = 0;

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

{    

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

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

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

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

y_delta = -CELL_Y_SIZE;

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

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

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

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

next_у_cell = -1;

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

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

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

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

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

{

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

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



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

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

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

x_delta = CELL_X_SIZE;

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

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

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

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

next_x_cell = 0;

}

else

{

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

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

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

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

x_delta = -CELL_X_SIZE;

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

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

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

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

next_x_cell = -1;

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

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

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

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

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

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

while(casting)

{

if (xray!=INTERSECTION_FOUND)

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

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

cell_y = (int)yi;

cell_y>>=CELL_Y_SIZE__FP;

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

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

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

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

yi_save = (int)yi;

xb_save = x_bound;

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

xray = INTERSECTION_FOUND;

casting--;

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

else

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

{

yi += y_step[view_angle];

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

x_bound += x_delta;

} // Конец else

}

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

if (уray !=INTERSECTION_FOUND)

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

cell x = xi;

cell_x>>=CELL_X_SIZE_FP;

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

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



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

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

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

xi_save = (int)xi;

yb_save = у_bound;

у_ray = INTERSECTION_FOUND;

casting--;

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

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

xi += x_step[view angle];

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

у_bound += у_delta;

} // Конец else

}

} // Конец while

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

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

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

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

if (dist_x < dist_y)

{

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

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

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

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

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

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

scale_row = scale_table[scale-1];

if (scale>(WINDOW_HEIGHT-1))

{

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

scale=(WINDOW_HEIGHT-l) ;

}

else

sliver_clip =0;

sliver_scale = scale-1;

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

sliver_texture = object.frames[x_hit_type];

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

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

sliver_ray     = ray;

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

Render_Sliver_32();

} // Конец if else

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

{

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

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

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

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

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

scale_row      = scale_table{scale-l];

if (scale>(WINDOW_HEIGHT-1)) {

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

scale=(WINDOW_HEIGHT-l);

}

else

sliver_clip = 0;

sliver_scale = scale-1;

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

sliver_texture= object.frames[y_hit_type+l];

sliver_column = (xi_save & 0x003F);

sliver_top     = WINDOW_MIDDLE - (scale >> 1) ;



sliver_ray     = ray;

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

Render_Sliver_32();

} // Конец else

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

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

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

if (++view_angle>=ANGLE_360)

{

view_angle=0;

} // Конец if

} // Конец for

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

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

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

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

Секция 1

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

Секция 2

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

Х % 64 == Х AND 63

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



Секция 3

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

Секция 4

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

Секция 5

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

Секция 6

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

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

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

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

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

Секция 7



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

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

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

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



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


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

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

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

{

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

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

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

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

char far *work_sprite;

int far *row;

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

unsigned char data;

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

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

if (scale>(WINDOW_HEIGHT-1))

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

scale=(WINDOW_HEIGHT-l);

sprite->y = 0;

}

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

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

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

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

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

{

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

offset  += SCREEN_WIDTH;

work_offset = row[y+scale_off];

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

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

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

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

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

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

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

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

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

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

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

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



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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

shl bx,8

mov ax,bx

shr bx, 2

add bx,ax

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

add di,bx

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

mov ax,sliver_scale

add ax,bx

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

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

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

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

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

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

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

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

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

; row = scale_table[scale]

mov dx, scale_row

shl bx,1

add bx, dx 

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

add dx,sliver_column

mov bx,cx

; offset      += SCREEN_WIDTH;

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

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

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

jne Sliver_Loop

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

pop si

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

Render_Sliver_32 ENDP END

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


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

Оттенение

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

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

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

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

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

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


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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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



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

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

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

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

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

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

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

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

ret                          ; конец

Draw_Ground_32 ENDP END

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

rep movsd                    ; копируем

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

ret                          ; сделано!

Render_Buffer_32 ENDP

END

Цикл игры

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



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

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

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

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

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

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

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

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


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

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

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

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

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

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

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

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

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

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

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

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

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

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


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

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

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



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

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

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

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

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


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

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

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

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

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

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


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

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

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

лучей.

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

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

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

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

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

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

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

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

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

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

Andromeda Industries

P.O. Box 641744

San Jose, CA 95164-1744


Игровой цикл


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

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

§

Проинициализировать все системы;

§          Начать игровой цикл;

§          Получить команды игрока;

§          Отработать логику игры и проделать все необходимые преобразования участвующих в игре объектов;

§          Визуализировать графическое изображение;

§          Вернуться к началу цикла.

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

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

Никто за вас не решит, как вы будете реализовывать свою игру.


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

§          Время, затраченное на реализацию игры;

§          Скорость выполнения программы;

§          Простота модификации;

§          Легкость отладки.

Этот список можно продолжить, однако это, пожалуй» наиболее важные факторы.

В четырнадцатой главе, «Связь», мы займемся игрой, которую я назвал Net-Tank. Это работающий через последовательный порт симулятор танкового боя, со звуковыми эффектами, потрясающей графикой, различным оружием и полностью интерактивным режимом игры. Цикл этой игры приведен на рисунке 12.4.



Как вы можете видеть, цикл игры включает в себя все обсуждавшиеся ранее компоненты:

§          Инициализацию объектов игры;

§          Удаление объектов;

§          Ввод данных локальным игроком;

§          Ввод данных удаленным игроком;

§          Выполнение преобразований;

§          Расчет столкновений;

§          Вызов подпрограммы, перемещающей снаряды;

§          Воспроизведение графического изображения в автономном буфере экрана. В нужный момент это изображение мгновенно возникнет на экране.

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

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


Игровые объекты


Предположим, у вас есть замечательная идея компьютерной игры и вы готовы написать 50000 или более строк программы на Си! Первая проблема, с которой вы столкнетесь при создании своей игры, будет связана с представлением объектов. Должны ли вы использовать массивы, структуры, связанные списки или что-то еще? Мой совет будет такой: «Начинайте с простого, мудрость приходит с опытом».

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

Старайтесь, чтобы выбранные вами структуры данных повышали эффективность работы программы. Например, пусть совершенно точно известно, что в вашей игре будет не более десяти объектов. Вы, конечно, можете создать связанный список и пополнять его по мере создания объектов. Это сэкономит память, но сделает программу более сложной. Столько хлопот ради десятка объектов - так ли уж это нужно? Простой массив в такой ситуации куда как уместнее!

Компьютерные игры настолько сложны, что если вы будете еще и сами усложнять простые вещи, то никогда не закончите работу. Выполните ее в черновом варианте, а затем, если хотите, возвращайтесь и совершенствуйте. У меня есть друг, который в 70-х или 80-х годах создал одну очень знаменитую компьютерную игру. Он всегда говорил мне: «Напиши игру, потом перепиши ее, а затем напиши еще лучше». Таким образом, начинайте с чернового варианта, а затем уже шлифуйте его.



Инициализация последовательного порта


Вот шаги, которым мы должны следовать:

§

Сначала нужно установить номера передаваемых битов, количество стоп битов и тип четности. Это делается с помощью регистра управления линии (LCR);                                             

§          Затем устанавливается скорость передачи загрузкой старшего и младшего байта делителя;

§          Далее нужно инициализировать UART для управления прерываниями;

§          Мы должны сообщить программируемому контроллеру прерываний ПК (PIC), какие прерывания по последовательному порту он должен допускать;

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

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

Работа с делителем немного запутана. Вы знаете, что регистры 0 и 1 выполняют дополнительные функции загрузки двухбайтного делителя, деление на который числа 115200 дает результат, используемый UART как окончательная скорость передачи. Однако, как мы знаем, регистры 0 и 1 являются соответственно регистром поддержки передачи (THR) и регистром прерывания (IER). Когда бит 7 регистра управления линией установлен в 1, они получают номера 8 и 9, но продолжают адресоваться как 0 и 1. Ясно?

В качестве примера, давайте установим скорость передачи в 9600 бод. Мы могли бы найти число, которое при делении на него значения 115200 давало бы 9600. Это число 12. Далее мы должны запихнуть его в младший и старший байты. В этом случае младший байт будет равен 12, а старший - 0.


Далее нужно установить бит 7 (DLAB) регистра управления линией в 1 и записать младший байт в регистр 0, а старший - в регистр 1, через которые переданные байты и попадут в регистры 8 и 9. После этого необходимо очистить бит 7 (DLAB) регистра управления линии. Это было бы не так уж и плохо, а?

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

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

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

Если вы помните, в двенадцатой главе, "Мнимое время, прерывания и мультизадачность", говорилось, что в Си несложно установить новое прерывание, используя ключевое слово _interrupt. Мы напишем процедуру, которая будет активизироваться всякий раз с приходом прерывания. Но как нам сообщить UART, что прерывание обнаружено? Если вы пристально посмотрите на описание регистров, то поймете, что вам необходимо установить бит здесь бит там, и UART будет делать свою работу. Чтобы назначить прерывания, мы должны установить следующие биты в UART:

• Бит 0 (RxRDY) регистра прерывания (IER) должен быть установлен в 1;

• Бит 3 (GP02) регистра управления модемом (MCR) должен быть установлен в 1.

После этого мы уже готовы принимать прерывания, правильно? Ошибаетесь! Нужно сделать еще одну вещь. Мы должны сообщить программируемому контроллеру прерываний (PIC), какие именно прерывания по последовательному порту он должен задействовать. Чтобы выполнить это, необходимо изменить установки в регистре маски прерывания (IMP) PIC'a, который доступен через порт 21h.Таблица 14.3 показывает обозначение битов IMR.

Таблица 14.3. Регистр маски прерывания (IMR) PIC'a.

Бит 0: IRQ0 - используется для таймера

Бит 1: IRQ1 - используется для клавиатуры

Бит 2: IRQ2 – зарезервирован

Бит 3: IRQ3 - COM2 или COM4

Бит 4: IRQ4 - СОМ1 или COM3

Бит 5: IRQ5 - жесткий диск

Бит 6: IRQ6 - гибкий диск

Бит 7: IRQ7 - принтер

Таким образом, последняя вещь, которую нам необходимо сделать для обработки прерываний и запуска - активировать нужное прерывание по биту 3 или 4. Но будьте осторожны! Регистр инвертирован, так что 0 означает включен, а 1 — выключен.

Осторожно

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


ИНСТРУМЕНТАЛЬНЫЕ СРЕДСТВА


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

§

Определение понятия инструментального средства;

§          Какие инструменты нам действительно необходимы;

§          Редакторы битового массива изображения;

§          Пакеты для оживления изображения;

§          Производство кинофильма;

§          Звуковые редакторы;

§          Редактор карты WarEdit;

§          Использование WarEdit;

§          Функциональное описание WarEdit;

§          Усовершенствование WarEdit.



Интерфейс пользователя


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

В компьютерных играх с помощью интерфейса решаются две .задачи:

§

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

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

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

Интерфейс пользователя должен удовлетворять следующим требованиям:

§          Быть доступным в любой момент игры;

§          Использовать слова, написанные большими буквами и не выводить на экран много пунктов настроек одновременно;

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

§          Учитывать возможность того, что игрок не знает, что делает.

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



ИСКУССТВЕННЫЙ ИНТЕЛЛЕКТ


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

Мы обсудим следующие темы:

§

Обзор того, как мыслят видеоигры;

§          Алгоритмы Преследования и Уклонения;

§          Шаблонные мысли;

§          Случайные передвижения;  

§          Автоматы с конечными состояниями;  

§          Вероятностные автоматы;

§          Память и обучение;   

§          Алгоритмы Поиска;                              

§          Теория игр.     



Использование функций драйвера для проигрывания VOC-файлов


Давайте подытожим разговор об используемых функциях. Вообще-то они потрясающе просты и вы наверняка сможете самостоятельно написать неболь­шую программу и поэкспериментировать с цифровыми каналами ввода-вывода звуковой карты Sound Blaster... Как бы не так! Неужели я вас брошу одних в этом цифровом чистилище? Ни в коем случае. Давайте-ка вместе загрузим драйвер и проиграем какой-нибудь VOC-файл. То, что у нас получится, вы можете впоследствии использовать в собственных играх.

Первое, что мы должны сделать, это загрузить CT-VOICE.DRV в память. Для этого нужно просто выделить некоторое количество памяти, открыть файл как двоичный и загрузить его байт за байтом. Есть, правда, одна проблема: драйвер должен быть загружен от границы сегмента. Это значит, что сегмент может, быть любым, но смещение начала драйвера должно быть нулевым. Ни одна функция из семейства allocate этого делать не умеет. Здесь необходима функция, которая способна резервировать память на границе параграфа. Такая функция называется _dos_allocmem(). Итак, нам надо сделать следующее:

§

Открыть файл CT-VOICE.DRV;

§          Определить его размер;

§          Отвести под него память;

§          Загрузить его.

Это делает функция, приведенная в Листинге 9.1.

Листинг 9.1. Выделение памяти для CT-VOICE.DRV.

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

void Voc_Load_Driver(void)

// загрузить ct-voice.drv

int driver_handle;

unsigned errno,segment,offset,num_para,bytes_read;

// открыть

файл драйвера

_dos_open("CT-VOICE.DRV", _O_RDONLY, &driver_handle);

// выделить

память

num_para = 1 + (filelength(driver_handle))/16;

_dos_allocmem(num_para, &segment);

//установить указатель на область данных драйвера

_FE_SEG(driver_ptr) = segment;

_FP_OFF(driver_ptr) = 0;

// загрузить

код драйвера

data_ptr = driver_ptr;

do {

_dos_read(driver_handle,data_ptr, 0х4000, &bytes_read);




data_ptr += bytes_read;

} while (bytes_read==0x4000);

// закрыть

файл

_dos_close(driver_handle);

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

Мы можем разбить функцию из Листинга 9.1 на три части:

§          Вначале мы открываем файл CT-VOICE.DRV в чисто двоичном режиме.Мы не  должны делать никаких преобразований символов - это было бы катастрофой! Мы же читаем реальный код, а не ASCII-файл;

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

§          Наконец, драйвер загружается по 32К за один прием. Эта одно из замечательных отличий функции _dos_read () от стандартной функции getch(): мы можем читать большие куски кода за один раз.

Теперь, после загрузки драйвера мы должны сохранить указатель так, чтобы впоследствии можно было найти начало драйвера. Этот указатель мы будем хранить в виде глобальной переменной с именем driver_ptr. (Я думаю, что это имя достаточно содержательно.)

Замечание

Давайте немного отвлечемся. Когда вы пишете компьютерные игры (или любое другое программное обеспечение), пожалуйста, используйте такие имена файлов и функций, которые несут смысловую нагрузку и отражают назначение объекта. Постарайтесь избегать таких имен, как t, j, k и им подобных. Используйте имена типа index_1, sprite_alive и так далее. Поверьте моему опыту: когда вы закончите писать компьютерную игру и вернетесь к ней через неделю, вы подумаете: «Не Фон Нейман ли это написал, да здесь сам черт ногу сломит!» Ведь если вы используете иероглифы вместо имен, кто кроме специалиста по иероглифам сможет в .них разобраться? Правильно? Тогда вернемся к нашим баранам.

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


То есть мы должны:

§          Открыть файл в бинарном режиме;

§          Отвести под него память;

§          Загрузить VOC-файл в отведенный буфер. Это делает функция Листинга 9.2.

Листинг 9.2. Загрузка VОС-файла.

char far *Voc_Load_Sound(char *filename,

unsigned char *header_length)

{ // загрузка звукового файла с диска в память

// и установка указателя на его начало

char far *temp_ptr;

char far *data_ptr;

unsigned int sum;

int sound_handle,t;

unsigned errno, segment, offset, num_para, bytes_read;

// открыть

звуковой файл

_dos_open(filename, _O_RDONLY, &sound_handle);

// выделить

память

num_para =1 + (filelength(sound_handle))/16;

_dos allocmem(num_para, &segment) ;

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

_FP_SEG(data_ptr) = segment;

_FP_OFF(data_ptr) = 0;

// загрузить звуковые данные

temp_ptr

= data_ptr;

do

{

dos_read(sound_handle,temp_ptr, 0х4000, &bytes__read) ;

temp_ptr += bytes_read;

sum+=bytes_read;

} while(bytes_read==0x4000);

// Проверить на всякий случай, звуковые ли это данные.

// Для этого проверяется присутствие слова "Creative".

if ((data_ptr[0] != 'С') || (data_ptr[1] != 'r'))

{

printf("\n%s is riot a voc file!",filename);

_dos_freemem(_FP_SEG(data_ptr) ) ;

return(0);

} // конец звукового файла

header_length = (unsigned char)data_ptr[20];

// закрыть

файл

_dosclose(sound_handle) ;

return(data_ptr) ;

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

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

Все остальные функции, которые нам потребуются, тривиальны. Мы используем ассемблерные вставки для настройки регистров и команду процессора CALL для вызова драйвера с помощью указателя driver_ptr.

Рассмотрим несколько таких функций, чтобы понять особенности их написания.


Драйвер должен быть инициализирован прежде, чем мы сможем его использовать. Это очевидное требование. Делать это мы должны с помощью функции 3, «Инициализировать драйвер». Листинг 9.3 содержит текст программы этой функции.

Листинг 9.3. Инициализация драйвера.

int Voc_Init_Driver(void)

{

// инициализация драйвера функция возвращает слово состояния

int status;

_asm

{

mov bx,3        ; функция инициализации драйвера имеет номер 3

call driver_ptr ; вызов

драйвера

mov status,ax   ; сохранение номера версии

} // конец ассемблерной вставки

// возвратить слово состояния

printf("\nDriver Initialized");

return(status);

} // конец

функции

Другая важная функций сообщает драйверу адрес переменной для передачи слова cостояния операции. Текст программы, которая устанавливает эту переменную (я назвал ее ct_voice_status), содержится в Листинге 9.4.

Листинг 9.4. Установка переменной слова состояния драйвера.

Voc Set_Status_Addr(char _far *status)

{

unsigned segm, offm;

segm = _FP_SEG(status);

offm = _FP_OFF(status) ;

asm{

mov bx,5        ; функция задания переменной слова состояния

; имеет номер 5

mov es,segm     ; в регистр ES

загружается сегмент переменной

mov di,offm   ; в регистр DI

загружается смещение переменной

call driver_ptr ; вызов

драйвера

} // конец ассемблерной вставки

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

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

Листинг 9.5. Проигрывание VОС-файла из памяти.

int Voc_Play_Sound(unsigned char far *addr,

unsigned char header_length)

{

// проигрывает загруженный в память VOC-файл

unsigned segm, offm;

segm = _FP_SEG(addr);

offm = _FP_OFF(addr) + header_length;

_asm{

mov bx,6        ; функция 6 - воспроизведение VOC-файла

mov ax,segm

mov es,ax       ; в регистр ES

загружается сегмент

mov di,offm     ; в регистр DI загружается смещение

call driver_ptr; вызов

драйвера

} // конец ассемблерной вставки



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

Функция Voc_Play_Sound из Листинга 9.5 работает следующим образом:

§          Адрес VOC-файла, который мы хотим проиграть, из памяти передается в функцию;

§          Затем функция использует две крайне полезные макрокоманды FP_SEG () и FP_OFF(), получая таким образом сегмент и смещение стартового адреса буфера с VOC-файлом;

§          Сегмент и смещение помещаются в регистровую пару ES:DI в соответствии с требованиями драйвера;

§          Вызывается драйвер.

И, пожалуйста - звучит музыка!

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

§          Все VOC-файлы расположены в текущем каталоге;

§          CT-VOICE.DRV также находится в текущем каталоге.

Последнее замечание по поводу воспроизведения оцифрованного звука: после того, как ваша программа начнет проигрывание мелодии, она может делать, что угодно. Лучше всего — продолжить игровой цикл. Операции по воспроизведению звука будут полностью выполняться картой Sound Blaster и аппаратным обеспечением прямого доступа к памяти. Вашей программе не нужно ничего делать, за исключением собственно запуска этого процесса (и затем - его остановки), а это занимает всего несколько микросекунд.

Листинг 9.6. Полная программа воспроизведения звука.

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

#include <io.h>

#include <stdio.h>

#include <stdlib.h>



#include <dos.h>

#include <bios.h>

#include <fcntl.h>

// ГЛОБАЛЬНЫЕ ПEPEMEHHЫE //////////////////////////////

char   far *driver_ptr;

unsigned version;

char _huge *data_ptr;

unsigned ct_voice_status;

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

void Voc_Get_Version(void)

{

// получить версию драйвера и вывести, ее на экран

_asni

{                                                                                                                            

mov bx,0        ; функция 0 возвращает номер версии

call driver_ptr ; вызов

драйвера

mov version,ax

; сохранить номер версии

} // конец

ассемблерной вставки

printf("\nVersion of Driver = %Х.0%Х",

((version>>8) & 0x00ff), (version&0x00ff));

}

// конец

функции

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

int Voc_lnit_Driver(void)

{

// инициализация драйвера функция возвращает слово состояния

int status;

_asm

{

mov bx,3        ; функция инициализации драйвера имеет номер 3

call driver_ptr ; вызов

драйвера

mov status,ах   ; сохранение номера версии

}// конец ассемблерной вставки

// возвратить слово состояния

printf("\nDriver Initialized");

return(status);

} // конец

функции

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

int Voc_Terminate_Driver(void)

{

// прекратить работу драйвера

_asm {

mov bx,9        ; функция 9 прекращает работу драйвера

call driver_ptr ; вызов драйвера

} // конец ассемблерной вставки

// освобождение памяти

_dos_freemem(_FP_SEG(driver_ptr));

printf("\nDriver Terminated");

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

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

void Voc_Set_Port(unsigned port) {

// установить адрес порта ввода/вывода Sound Blaster

_asm

{

mov bx,l        ; функция 1 устанавливает адрес порта ввода/вывода

mov ax,port     ; поместить адрес порта ввода/вывода в регистр АХ

call driver_ptr ; вызов драйвера

} // конец ассемблерной вставки

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



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

void Voc_Set_Speaker(unsigned on)

{

// включить/выключить вывод звука

_asm {

mov bx,4       ; функция 4 включает или выключает вывод звука

mov ах,on       ; поместить флаг включить/выключить в регистр АХ

call driver_ptr ; вызов драйвера

} // конец ассемблерной вставки

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

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

int Voc_Play_Sound(unsigned char far *addr,

unsigned char header_lehgth)

{

// проигрывает загруженный в память VOC-файл

unsigned segm,offm;

segm = _FP_SEG(addr);

offm = _FP_OFF(addr) + header_length;

asm

{

mov bx,6        ;Функция 6: воспроизведение"VOC-файла

mov ax,segm

mov es,ax       ; в регистр ES загружается сегмент

mov di,offm     ; и регистр DI загружается смещение

call driver_ptr ; вызов драйвера

} // конец ассемблерной вставки

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

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

int Voc_Stop_Sound(void)

{

// прекращает воспроизведение звука

_asm

{ mov bx,8        ; функция 8 прекращает воспроизведение звука

call driver_ptr ; вызов драйвера

} // конец ассемблерной вставки

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

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

int Voc_Pause_Sound(void)

{

// приостанавливает воспроизведение звука

_asm

{

mov bx,10       ; функция 10 приостанавливает

                ; воспроизведение звука

call driver_ptr ; вызов драйвера

} // конец ассемблерной вставки

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

int Voc_Continue_Sound(void)

{ // продолжает воспроизведение приостановленного звука

asm

{

mov bx,11        ; функция 11 продолжает воспроизведение

                 ; приостановленного звука

call driver_ptr  ; вызов драйвера

} // конец ассемблерной вставки

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

int Voc_Break_Sound(void)

{

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

_asm

{

mov bx,12       ; функция 12 прерывает цикл воспроизведения звука



call driver_ptr ; вызов

драйвера

} // конец ассемблерной вставки

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

void Voc_Set_DMA(unsigned dma)

{

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

_asm

{

mov  bx,2        ; функция 2 устанавливает номер прерывания

                 ; прямого доступа к памяти

mov ax,dma       ; поместить в регистр АХ

                 ; номер прерывания прямого доступа в память

call driver_ptr  ; вызов драйвера

} // конец ассемблерной вставки

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

Voc Set_Status_Addr(char _far *status)

{

unsigned segni,offm;

segm = _FP_SEG(status);

offm = _FP_OFF(status);

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

asm               

{

mov bx,5        ; функция задания переменной слова состояния

                ; имеет номер 5

mov еs, segm    ; в регистр ез загружается сегмент переменной

mov di, offm    ; в регистр di загружается смещение переменной

call driver_ptr ; вызов

драйвера

} // конец ассемблерной вставки

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

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

void Voc_Load_Driver(void) {

// загрузить CT-VOICE.DRV

int driver_handle; unsigned errno,segment_offset,num_para,bytes_read;

// открыть

файл драйвера

_dos_open("CT-VOICE.DRV", _O_RDONLY, &driver_handle);

// выделить

память

num_para= 1 + (filelength(driver__handle))/16;

_dos_allocmem(num_para, &segment);

// установить указатель на область данных драйвера

_FP_SEG(driver_ptr) = segment;

_FP_OFF(driver_ptr) =0;

// загрузить

код драйвера data_ptr = driver_ptr;

do

{

_dos_read(driver_handle,data_ptr, 0х4000, &bytes_read);

data_ptr += bytes_read;

} while (bytes_read==0x4000);

// закрыть

файл

_dos_close(driver_handle);

} // конец

функции

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

char far *Voc_Load_Sound(char *filename,

unsigned char *header_length)

{ // загрузка звукового файла с диска в память



// и установка указателя на его начало

char far *temp_ptr;

char far *data_ptr;

unsigned int sum;

int sound handle,t; unsigned errno, segment, offset, num_para, bytes_read;

// Открыть

звуковой файл

_dos_open(filename, _O_RDONLY, &sound_handle);

// Выделить

память

num_para = 1 + (filelength(sound_handle))/16;

_dos_allocmem(num_para,&segment);

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

_FP_SEG(data_ptr) = segment;

_FP_OFF(data_ptr} = 0;

// Загрузить звуковые данные

temp_ptr

= data_ptr;

do

{

_dog_read(sound_handle,temp_ptr, 0х4000, &bytes_read) ;

temp_ptr += bytes_read;

sum +=bytes_read;

} while (bytes_read==0х4000);

// проверить на всякий случай, звуковые ли это данные;

// для этого проверяется присутствие слова "Creative"

if ((data_ptr[0] !='C') || (data_ptr[1] != 'r'))

{

printf("\n%s is not a voc file!",filename);

_dos_freemem(_FP_SEG(data_ptr));

return(0);

} // конец звукового файла

*header_length = (unsigned char)data_ptr[20];

// закрыть

файл

_dos_close(sound_handle) ;

return(data_ptr);

} //конец

функции

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

void Voc_Unload_Sound(char far *sound_ptr) {

// удаление звуковых данных из памяти

_dos_freemem(_FP_SEG(sound_ptr));

) // конец

функции

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

void main(void)

{

char far *sounds[4];

unsigned char lengths[4];

int done=0,sel;

Voc_Load_Driver();

Voc_Init_Driver ();

Voc_Set_Port (0х220);

Voc_Set_DMA(5) ;

Voc_Get_Version();

Voc Set_Status_Addr((char _far *) &ct_voice_status) ;

// загрузка

звуковых файлов

sounds[0] = Voc_Load_Sound("beav.voc" , & lengths[0]);

soundsll] = Voc_Load_Sound("ed209.voc", &lengths[1]);

sounds[2] = Voc_Load_Sound{"term.voc",   &lengths[2]);

sounds[3] = Voc_Load_Sound("driver.voc"f &lengths[3]);

Voc_Set_Speaker(1);

// главный цикл событий;.позволяет пользователю

// выбрать звуковой файл.



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

while(!done)

{

printf("\n\nSound Demo Menu");

printf("\nl - Beavis");

printf("\n2 - ED 209") ;

printf("\n3 - Terminator");

printf("\n4 - Exit");

printf("\n\nSelect One ? ");

scant("%d",&sel);

switch (sel)

{

case 1:

{

Voc_Stop_Sound();

Voc Play_Sound(sounds[0] , lengths[0]);

} break;

case 2:

{

Voc_Stop_Sound();

Voc_Play_Sound(sounds[1],  lengths[1]);

} break;

case 3:

{

Voc_Stop_Sound(l ;

Voc_Play_Sound(sounds[2] , lengths[2]);

} break;

case 4:

{

done = 1;

} break;

default:

{

printf("\nFunction %d is not a selection.",sel) ;

} break;

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

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

// закончить работу

Voc_Play_Sound(sounds[3], lengths[3]);

// ожидание окончания звуковой последовательности

// слово состояния имеет значение -1 при воспроизведении звука

// и 0 - в;противном случае.

while(ct_voice_status()) {}

Voc_Set Speaker(O);

// выгрузить

звуковые файлы

Voc_Unload_Sound(sounds[0]);

Voc_Unload_Sound(sounds[1]) ;

Voc_Unload_Sound(sounds[2]);

Voc_Unload_Sound(sounds[3]);

Voc_Terminate_Driver ();;

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


Использование матриц. Двухмерная проекция


Двухмерную проекцию (2-D), иначе называемую картезианской, можно сравнить с листом бумаги в клеточку.

Каждая точка в двухмерной проекции однозначно описывается двумя координатами. Обычно эти координаты обозначаются буквами х и у, где х определяет точку на горизонтальной оси X, а у задает точку на вертикальной оси Y. Например, если нам захочется нарисовать точки (1,1) и (-3,4), то мы сделаем так, как это показано на рисунке 4.1.

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



Использование матриц в играх


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

Прелесть матриц состоит в том, что вы можете объединить все операции в одну матрицу, описывающую перемещение, масштабирование и вращение.

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



Использование сигнала вертикальной синхронизации


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

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

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

К счастью, карты VGA имеют специальный регистр, который отражает обратный ход луча. Он называется регистром состояния VGA и его можно прочесть, обратившись к порту 0x3DA. Из 8 битов, содержащихся в этом регистре, нас интересует четвертый справа бит (3d):

§          Когда бит установлен в 1, происходит обратный вертикальный ход луча;

§          Когда бит сброшен в 0, происходит перерисовка экрана.

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


Итак, мы можем написать подпрограмму, которая будет запрашивать статус бита и отслеживать начало процесса обратного вертикального хода луча. После этого она вернет управление основной программе, которая будет знать, что следующую 1/70-ю долю секунды для VGA (и 1/60-ю секунды для EGA), можно совершенно спокойно писать в видеобуфер.

Я написал демонстрационную программу VSYNC.С, которая выполняет подобную синхронизацию и вычисляет количество периодов обратного вертикального хода луча как функцию от времени. Так как за секунду выполняется 70 циклов регенерации экрана, то после минутной работы программы результат должен оказаться равным 4200. Листинг 7.5. содержит исходный текст этой программы.

Листинг 7.5. Подсчет количества циклов обратного вертикального хода луча (VSYNC.С).

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

#include <dos.h>

#include <bios.h>

#include <stdio.h>

#include <math.h>

#include <conio.h>

#include <graph.h>

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

#define VGA_INPUT_STATUS_1   0x3DA // регистр

состояния

VGA,

// бит 3 - сигнал вертикальной синхронизации

// 1 - происходит обратный вертикальный ход луча

// 0 - нет обратного вертикального хода луча    

#define VGA_VSYNC_MASK 0х08

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

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

unsigned char far *video_buffer

= (char far

*)0xA0000000L;// указатель на видеопамять

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

void Wait_For_Vsync(void)

{

// эта функция ожидает начала обратного вертикального хода луча, а

// если луч уже совершает обратный ход - ожидает следующего цикла

while (_inp(VGA_INPUT_STATUS_1) & VGA_VSYNC_MASK)

{

// обратный вертикальный ход луча - ничего не делаем

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

// ожидаем прихода сигнала vsync и возвращаем

// управление вызвавшей функции

while (! (_inp(VGA_INPUT_STATUS_l) & VGA_VSYNC_MASK) )

{

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



// хода луча - ничего не делаем

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

// начался обратный вертикальный ход луча –

// возвращаем управление вызвавшей функции

} // конец Wait_For_Vsync

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

void main(void)

{

long nuinber_vsyncs=0;  // хранит количество циклов

// обратного вертикального хода луча

while(!kbhit())

{

// ожидание vsync

Wait_For_Vsync(};

// выводим графику или что-либо еще пока происходит

// обратный вертикальный ход луча и мы имеем в своем

// распоряжении 1/70 секунды! Обычно в это время выполняется

// копирование дублирующего буфера в видеопамять

// увеличивается количество циклов vsyncs

number_vsyncs++;

// вывод на экран

_settextposition(0,0) ;

printf ("Number of vsync's = %ld   ",number_vsyncs);

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

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

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

§          Мы можем использовать это время в качестве основы для некоторых процессов или событий;

§          Мы можем использовать это для того, чтобы игровой цикл выполнялся с частотой 70Гц на любой машине;

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

Замечание

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

Перейдем к следующей интересной и захватывающей теме: мультипликации с помощью регистров цвета.


Использование уравнения плоскости для вершин многоугольника


А каким образом мы можем составить уравнение плоскости, зная только вершины многоугольника? Очень просто: так как все вершины прямоугольника принадлежат одной плоскости, мы можем взять две смежные вершины и построить к ним вектор нормали. Рисунок 6.18 показывает, как это сделать.

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

Имея вектор нормали к многоугольнику, уравнение плоскости находит Z-компонент для любой точки (х,у). При этом заданы: искомая точка (х,у) и вектор нормали к многоугольнику <Nx,Ny,Nz>:

                       Nz

Z = ---------------------------

        1- Nx * X – Ny * Y



Использование встроенного (in-line) ассемблера


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

§          Во-первых, программисту на Си теперь не нужно знать так много обо всем, как прежде;

§          Во-вторых, встроенный ассемблер — это максимальное удобство в программировании на разных языках.

Встроенный ассемблер объявляется следующим образом:

_asm{

инструкции ассемблера;

}

Это все, что надо: ни директив, ни пролога, ни эпилога. Только одно ключевое слово _asm, да пара скобок - и мы у цели.

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

§          Не надо заботиться о сохранении регистров — это сделают за вас;

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

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

void Swap(int num_1, int num_2)

{

_asm{

mov AX,num 1

mov BX,num 2

mov num 1,BX

mov num_2,AX

}

}

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

Для большинства задач возможностей встроенного ассемблера вполне достаточно.

список его функциональных возможностей намного


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

К специалистам в написании компьютерных игр часто обращаются с вопросом: «А насколько быстро это работает?» Так вот, встроенный ассемблер работает практически так же быстро, как и «настоящий». В большинстве случаев вы не увидите абсолютно никакой разницы. Единственное, в чем встроенный ассемблер уступает MASM'y, так это в скорости входа и выхода из функции, поскольку он содержит обязательные для Си-функций пролог и эпилог.

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

И, наконец, маленькая дополнительная информация. Раньше мы решили, что будем использовать процессоры старших моделей только как быстрый 8086. Однако, если вы хотите, весьма неплохо использовать во встроенном ассемблере инструкции процессора 80286, соответствующим образом изменив настройки компилятора. Конечно, можно также использовать команды процессоров 386, 486, 586. Только прежде подумайте: если вы будете ориентироваться на процессор 586, не окажется ли потенциальный рынок для вашей игры слишком ограниченным...


Использование WarEdit


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

Управление WarEdit несложно. Чтобы начать рисовать изображаемы уровень, необходимо выполнить следующие действия:

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

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

Я предлагаю прежде всего нарисовать все стены и двери. Когда уровень, готов, разместите ваши собственные игровые объекты. Если вы сделаете ошибку, то используйте правую кнопку мыши как стирательную резинку.

Когда вы удовлетворены уровнем, выполните следующие действия:

1.       Щелкните по кнопке START в нижнем правом углу.

2.       Поставьте одиночную точку где-нибудь в игровой области. Из этого места игрок начнет свой путь. Учтите, что в самом начале игры он всегда будет смотреть в направлении верха редактируемого поля.

Чтобы загрузить или сохранить изображение, используйте управление в верхней правой части интерфейса. Имена файлов могут быть до 8 символов длиной без учета расширения. Чтобы начать все с начала, (то есть для очистки экрана), щелкните по кнопке CLEAR. Экран очистится.

Вот и все о том, как использовать редактор.



Использование звука в играх


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

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

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

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

Алгоритм 9,1. Псевдокод планировщика звуков.

while(true)

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

// Если звук уже неактуален, удалить его из очереди. Если нет

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




// первого звука в очереди

else

// если запрашиваемый звуковой эффект имеет более высокий

// приоритет, чем текущий звук (например, игрок выстрелил

// из ружья) - прервать текущий звук и начать исполнение

// эффекта с более высоким приоритетом

else

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

// чем текущий звук, установить его в очередь

end

Алгоритм 9.1 - весьма хороший черновой пример планировщика. Он позволяет организовать очередь звуков и контролировать их исполнение.

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

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

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

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

§

Мы можем просто отсечь звук по границе восьми или шестнадцати бит;

§          Мы можем брать только процентное соотношение волн, что гарантирует попадание их суммы в нужные нам пределы.

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

Алгоритм 9.2. Псевдокод наложения звуков.

length one=length (sound one)

length two=length (sound two)

if length one > length two

// обработать первую часть звука



for index== 0 to length two new sound [index] = .5 * sound onetindex]+ .5 * sound two[index]

// обработать оставшуюся часть звука, // просто копируя его с уменьшением for index=length two+1 to length one

new sound [index] =.5 * sound one[index]

else

// обработать первую часть звука

for index=0 to length one

new sound [index] =.5 * sound one[index]+ .5 * sound two[index]

// обработать оставшуюся часть звука,

// просто копируя его с уменьшением

for index= length one+1 to length two

new sound [index] =.5 * sound two[index] end

По сути, Алгоритм 9.2 — это все, что вам нужно для того, чтобы сложить звуки вместе. Конечно, здесь складываются только два звука, но алгоритм легко может быть приведен к более общему виду. Проблема только в том, что вам потребуется больше времени и памяти. Если у вас в память загружены два VOC-файла по 60К, вы должны будете сделать 60000 сложений, на что уйдет несколько миллисекунд. Это вполне терпимо, но результат сложения нужно поместить в новый буфер размером 60К. Так что, будьте осторожны!


В этой главе мы узнали


В этой главе мы узнали кое-что о том, как и для чего пишутся видеоигры- Кроме того, выяснилось, что помимо технических навыков для разработки игры нужно иметь:
§
Сценарий игры;
§          Распределение по ролям;
§          Дизайн игры;
§          Понимание того, для чего вы все это делаете.
Итак, вроде, мы обсудили все, что находится вокруг игр. Пора начать работать. Давайте начнем.

Эта глава многому нас научила.


Эта глава многому нас научила. За короткое время мы узнали уйму нового:
§          Мы изучили архитектуру семейства процессора 80х86;
§          Мы узнали, как использовать новые директивы макроассемблера для облегчения написания ассемблерных процедур;
§          Мы узнали, как вызывать функции на ассемблере из Си-программ;
§          Наконец, мы написали несколько программ, которые переводят компьютер в режим 13h, и посмотрели, как организован их вызов из Си.
Теперь вы стали намного в более близких отношениях с masm'om и языком ассемблера для ПК вообще. Мы обсудили все основные темы, которые важны при программировании видеоигр, включая интерфейс с языком Си и встроенный ассемблер.
Без сомнения, мне надо было научить вас основам программирования на ассемблере. Вы должны уметь использовать ассемблер для решения разных Задач. В этой книге мы еще не раз с ним столкнемся, и вы должны быть уверены, что не утонете в море единиц и нулей. Если вы считаете, что еще не все поняли, то лучше возьмите и перечитайте какую-нибудь хорошую книгу об ассемблере, а уж потом переходите к изучению следующей главы.

В этой главе мы изучили


В этой главе мы изучили разные устройства ввода, которые могут использоваться в видеоиграх: клавиатура, джойстик, мышь. Более того, мы даже написали функции для работы с этими устройствами. Еще мы выяснили, что не стоит использовать BIOS для работы с устройствами ввода, за исключением клавиа­туры.
МЕХАНИЗМЫ ДВУХМЕРНОЙ ГРАФИКИ
Вы можете считать, что экран компьютера подобен листу бумаги, который можно изгибать, сворачивать в трубочку и вообще как угодно трансформировать в руках. Чтобы делать подобные вещи, мы должны знать язык, с помощью которого все это выполняется — язык математики.
В данной главе мы познакомимся с тем, что называется двухмерная проекция и изучим следующие темы:
§          Картезианская, или двухмерная проекция;
§          Точки, линии и области;
§          Трансляция;
§          Масштабирование;
§          Повороты;
§          Разрезание;

Эта глава могла бы оказаться


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

Эта глава, наверное, самая важная


Эта глава, наверное, самая важная в книге. Среди всей информации, которая, может быть, не относится напрямую к играм, вы узнали общую концепцию построения видеоигр, что абсолютно необходимо для их написания. Так что, если вы чего-то не поняли, то остановитесь и прочтите главу вновь (не беспокойтесь, я вас подожду).
Если вы прочитали и поняли в этой главе все, то .должны знать, что:
§
Мы изучили VGA-карту и ее архитектуру;
§          Мы узнали о режиме l3h, который дает лучшее разрешение и наиболее прост для программирования;
§          Мы узнали, как в режиме l3h программировать регистры цвета, рисовать пиксели, загружать PCX-файлы и перемещать битовые образы;
§          Мы поговорили о спрайтах и создали простую библиотеку для работы с ними;
§          Мы затронули довольно сложные вещи, такие как дублирующий буфер и
§          вертикальная синхронизация, о которых более подробно мы поговорим чуть позже;
§          И, наконец, собрав все вместе, мы сделали демонстрационную программу, рисующую маленького ковбоя, который ходит по городу.
До встречи в новой главе.
ТРЕТЬЕ ИЗМЕРЕНИЕ
Последние достижения в области электроники и программирования приблизили наши фантазии о трехмерных видеоиграх к осязаемой реальности. Разработчики игр сегодня могут создать трехмерные игры для недорогого ПК, позволяющие игрокам получить ощущения, всего несколько лет назад казавшиеся недоступными,
Говоря по правде, и в прошлом были трехмерные имитаторы полетов и несколько пространственных игр. Но все они были очень медленными и нуждались в мощных компьютерах. Естественно, это резко сужало круг потенциальных игроков. Все эти ограничения заставляли программистов искать пути создания нетрадиционных методов рен-деринга.


Эти новые способы были очень просты и имели производительность, еще пару лет назад считавшуюся невозможной.
В данной главе мы сначала обсудим традиционные методы рендеринга для ПК. а затем погрузимся в технику, называемую «отсечение лучей». Этот метод осуществляет рендеринг со скоростью, близкой к световой...
Из этой главы вы узнаете:
§                      Что такое трехмерное пространство
§                      Как изображаются точки, многоугольники и объекты в трехмерном пространстве;
§                      Масштабирование, трансляция и повороты в трехмерном пространстве;
§                      Проекции;
§                      Геометрическое моделирование;
§                      Алгоритм удаления невидимой поверхности;
§                      Алгоритм художника;
§                      Алгоритм, использующий Z-буфер;
§                      Что такое текстуры;
§                      Метод трассировки лучей;
§                      Алгоритм отсечения лучей;
§                      Реализация алгоритма отсечения лучей;
§                      Усовершенствование алгоритма отсечения лучей;
§                      Освещение, затемнение и цветовая палитра.
§                       
Мы начнем наш урок с основных концепций трехмерного пространства, чтобы кое-что вспомнить и понять. Затем мы бросимся вперед, чтобы узнать как создаются игры типа Wolfenstein, DOOM и Terminator Rampage!

В этой главе мы изучили


В этой главе мы изучили основы трехмерной графики, поговорили о математических основах трехмерных трансформаций и методах, используемых для обсчета трехмерных образов (удаление невидимых поверхностей и т. д.).
Кроме этого, мы узнали о двух наиболее важных с точки зрения создания красивых DOOM-образных игр методах: о трассировке и отсечении лучей. Мы даже написали реализацию алгоритма отсечения лучей.
Позже, когда настанет пора написать игру Warlock, мы еще раз вспомним эти методы и реализуем все, о чем мы говорили в этой главе, в полном объеме. А пока давайте перейдем к следующей главе. Кстати, пока не забыл, если вы что-то не поняли, прочитайте и осмыслите главу еще раз.
УЛУЧШЕННАЯ БИТОВАЯ ГРАФИКА И СПЕЦИАЛЬНЫЕ ЭФФЕКТЫ
Границу между просто хорошей и отличной игрой провести довольно сложно. Ведь разнообразие алгоритмов, сюжетов и звуков, применяемых в играх, невелико. В чем же причина того, что одна игра становится бестселлером, а другая - нет? Все дело в нюансах: более искусный переход от одной картинки к другой, мягкая прокрутка, точная синхронизация и т. д. Чтобы добиться этого, мы должны постоянно оттачивать наше мастерство, как воин - свой меч.
В этой главе вам встретятся некоторые Действительно интересные (и, вообще-то, довольно простые) программы. Они создают такие же сложные и реалистичные эффекты, как те, которые вы наверняка видели в компьютерных играх. Также на этих страницах вы узнаете о таком сложном приеме работы с растровой графикой, как масштабирование. Итак, в этой главе:
§          Ускорение процесса двоичного кодового преобразования (бит-блиттинга);
§          Применение логических операций;
§          Кодирование прозрачности;
§          Битовое отсечение;
§          Контроль столкновения спрайтов;
§          Дублирующая буферизация;
§          Использование сигнала вертикальной синхронизации;
§          Мультипликация с помощью цветовых регистров;
§          Освещение ваших игр;
§          Связь мультипликации с контекстом;
§          Мультипликационное движение («animotion»);
§          Прокрутка;
§          Специальные эффекты;
§          Текстуры;
§          Масштабирование растровых изображений;
§          Повороты растровых изображений.
Итак, больше дела, меньше слов!

В этой главе мы затронули


В этой главе мы затронули много тем, относящихся к серьезной работе с растровой графикой и специальными визуальными эффектами, но, самое главное, мы впервые в этой книге начали разговор о том, как сделать так, чтобы игрок получил от вашей игры удовольствие.
Я думаю, многие программисты уже забыли, что это такое, променяв его на мегабайты оцифрованного звука и графики.
Поэтому не будем забывать об удовольствии!
ВЫСОКОСКОРОСТНЫЕ ТРЕХМЕРНЫЕ СПРАЙТЫ
На самом деле, новых понятий в этой главе не появится. Скорее, речь пойдет о практическом применении тех технологий и концепций, которые мы обсудили в предыдущих главах. Обычно я никогда не рассматриваю практическую реализацию в книгах подобного плана. Это связано с тем, -что создание программы - скорее, творческий, чем технологический процесс. Однако в данном случае я изменил свое решение, так как хочу, чтобы вы четко разобрались в ряде основополагающих концепций.
За последние несколько лет мир компьютерных игр пережил несколько сенсаций (и появление игры DOOM одна, но не единственная, из них). К этим сенсациям, в том числе следует, отнести и появление на свет игры Wing Commander, написанной Крисом Робертсом (Chris Roberts). Увидев ее впер вые, я был так потрясен, что вскоре она стала моей любимой игрой. В Wing Commander и других играх подобного плана была использована та же технология визуализации спрай тов, что и в играх типа DOOM,
Образы в Wing Commander - это или простые макеты, или создаваемые компьютером объекты, преобразованные в цифровую форму двухмерного спрайта.. При движении по экрану эти спрайты видоизменяются по определенным математическим законам, создавая полное ощущение перспективы. Однако, хотя с формальной точки зрения данные изображения и не являются трехмерными, над ними производятся стандартные процедуры удаления невидимых поверхностей и другие математические расчеты для трехмерных изображений. Другими словами, это проекции трехмерных объектов на плоские многоугольники, которые собственно, и являются спрайтами.


Для изготовления игры типа Wing Commander не требуется сверхсложной математики. Однако подобная игра содержит массу тонкостей, хитрых приемов и замечательных алгоритмов. Посмотрим, сможем ли мы разобраться в некоторых из них, обсудив следующие вопросы:
§
Механика трехмерных спрайтов;
§          Аксонометрические преобразования;
§          Правильный расчет масштаба;
§          Видимый объем;
§          Новая версия масштабирования;
§          Отсечение в трехмерном пространстве спрайтов;
§          Построение траекторий;
§          Удачный угол зрения на спрайты;                             
§          Трехмерное звездное небо;                                
§          Оцифровка объектов и моделирование;                   
§          Создание съемочной студии;                                   
§          Цветовая палитра.                                                 

В этой главе мы поговорили


В этой главе мы поговорили об искусстве озвучивания игр для персонального компьютера;
§          Мы изучили Sound Blaster и исполнение на нем оцифрованных звуков, с использованием специального драйвера CT-VOICE.DRV, поставляемого фирмой Creative Labs;
§          Мы рассмотрели основы теории звуков и возможности Sound Blaster;
§          Мы поговорили о планировщике звуковых эффектов и о ряде других достаточно сложных вещей.
Настало время поговорить о музыке. Встретимся в следующей главе!

В этой главе был представлен


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

В этой главе мы затронули


В этой главе мы затронули массу интересных тем и даже создали небольшой "мозг", способный имитировать многие функции, присущие живым объектам. Используя комбинации различных алгоритмов, описанных в этой главе, вы сможете создать «виртуальный мозг» достаточно мощный для того, чтобы ваши издания смогли бороться за выживание. Однако, я хочу вам дать пару советов:
§          Старайтесь не усложнять «мозг» без необходимости;
§          Не пытайтесь сразу создать суперкомпьютер. Вначале пусть ваше первое произведение будет больше похоже на «Денди».
Мы обсудили большинство методов управления поведением, применяемых в сегодняшних видеоиграх. Если вы смогли в них разобраться, вы сможете создавать игры не хуже, а то и лучше чем те, которые существуют сегодня. Индустрия компьютерных игр движется семимильными шагами по пути улучшения графических и звуковых возможностей, часто забывая о такой маленькой детали как интеллект. К счастью, теперь вы имеете возможность привнести «мозги» и стратегию в следующее поколение компьютерных игр.

Поскольку сейчас происходит бурный рост


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

Мы не только научились работать


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

и если вы все еще


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

Изменение частоты таймера


Давайте поговорим о том, как изменять частоту таймера. Таймер генерирует прерывания следующим образом: заданное значение регистра счетчика таймера уменьшается на единицу каждый раз, когда приходит импульс от генератора. Когда значение счетчика становится равным нулю, таймер генерирует прерывание, после чего восстанавливает исходное значение счетчика. Затем процесс повторяется вновь. Счетчик 0 генерирует прерывания 18.2 раза в секунду или через каждые 55.4 миллисекунды. Это время задается следующим образом: на микросхему таймера подаются импульсы с частотой 1.19318МГц. В случае системного таймера в регистр помещается число 65536. Если взять 1.19318 и разделить па 65536 получится 18.206 циклов в секунду или 54.925 миллисекунд. Как видите, создатели персонального компьютера просто использовали макси­мально возможный делитель, который может поместиться в 16 битов: 65535. (На самом деле таймер «тикает» на один раз больше — вот откуда я взял число 65536).

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

Таблица 12.3. 16-разрядные значения счетчика для наиболее употребительных частот таймера.

Шестандцатиричное



Экранные эффекты


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

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

Программировать эффектные способы смены экрана на самом деле достаточно просто. Большинство из алгоритмов требует всего нескольких строк кода, а самые сложные из них не более дюжины строк. Это еще одна область, в которой вы должны будете проявить все свои таланты, чтобы добиться действительно стоящих результатов. Для примера я объединил три различных варианта исчезновения содержимого экрана в программу SCREENFX.C. Текст этой программы приведен в Листинге 7.9. Она использует функции работы с изображениями в формате PCX (созданные в пятой главе, «Секреты VGA-карт») и некоторые функции из библиотеки GRAPH0.C.

Программа SCREENFX.C демонстрирует три эффекта:

§

Гаснущее изображение;

§          Исчезновение изображения по точкам;

§          Оплывание изображения.

Попытаемся кратко разобраться в механизмах каждого из них, чтобы понять основные идеи экранных эффектов вообще.



Экраны для отражения статуса и изменения конфигурации игры


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

§

Текущий статус уровня, который занимает игрок;

§          Общий статус игры;

§          Экран конфигурации игры;

§          Экран помощи;

§          Командный экран;

§          Экран для выбора уровня сложности игры.



Эксперименты с макетами


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



К вашему сведению


Запомните, что все это мы делаем для того, чтобы научиться писать видеоигры, особенно трехмерные. И делаем мы это постепенно. Если я вам сразу покажу, как писать игры типа Wolfenstein 3-D или DOOM, то мы многое потеряем в тактике и философии разработки и создания игр.

Если же вы настоящий разработчик игр, то уже в данный момент должны думать,о том, как переделать те программы, которые я дал, в настоящую игру. Не беспокойтесь! Мне и в голову не приходит сказать: «В качестве упражнения перепишите программу из Листинга 4.11 в настоящую игру». Мы напишем игру вместе, но чуть позже. Лучше поговорим о том, какие изменения нам надо внести, чтобы игра получилась.

Если вы хотите узнать, как я бы переделывал Листинг 4.8 в настоящую игру, то я бы сделал следующее:

§          Оснастил программу всем необходимым для выполнения миссии. Ведь нужно иметь маневренный космический корабль, несколько ракет для уничтожения астероидов, а чтобы игра приняла «товарный» вид, нужна система подсчета очков, заставка и, конечно, возможность выхода из программы. Кроме перечисленного потребуется создать систему оповещения о столкновениях с астероидами (такая система будет рассмотрена чуть позже, в этой же главе);

§          Далее я подготовил бы свой корабль. Для этого описал бы объект, который позаимствовал, например, из игры Star Trek;

§          Написал бы подпрограмму для рисования и уничтожения корабля (типа функции DrawAsteroids из Листинга 4.11);

§          Разработал бы функции, позволяющие игроку управлять кораблем;

§          Написал бы функции, которые, в зависимости от состояния клавиатуры, вращали корабль по или против часовой стрелки. Это я сделал бы, изменяя величину angles в структуре, описывающей объект корабля;

§          Потом мне понадобился бы способ передвижения корабля. Для этого я бы изменял значения x_velocity и y_velocity в соответствующих функциях:

x_veiocity = cos(angle)*speed

y_velocity = sin(angle)*speed

§          где скорость изменялась бы при нажатии на определенные клавиши;

§          Чтобы ввести корабль в игру, я поместил бы вызов функции стирания всех графических объектов - корабля и астероидов - в одно и то же место. Затем поместил бы функцию движения, которая опрашивает клавиатуру, в середину программы (между вызовом функций стирания и рисования);

§          В самый конец цикла вставил бы функцию рисования корабля.

Вот, собственно, и все. Выглядит не очень сложно. Посмотрим, что дальше.



Кадры для привлечения внимания


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



Как?


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

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

1.

Поддерживает ли каждая нота эмоциональную направленность программы?

2.       Повышает ли музыка интерес к программе?

3.       Можете ли вы под нее танцевать?

Ну что ж, неплохо посидели. Надо бы встречаться почаще.



Как мыслят видеоигры: обзор


Придет тот день, когда компьютер станет таким же серьезным собеседником, как и лучшие человеческие умы. Однако сегодняшние компьютеры (по крайней мере, ПК) не обладают сложностью, необходимой для инициации мыслей и творческих процессов. Но на самом деле нас это и не должно волновать! Мы же делаем видеоигры, а не андроидов. В игре у нас присутствуют некоторые создания и объекты. Все, что нам нужно сделать, это придать им видимость способности разумного мышления. Играющий может ощущать себя помещенным на короткое время в некое пространство, где он думает, что вражеская атака на корабль реальна! Для осуществления этих целей мы должны проанализировать, каким разумом мы должны наделить наши игровые объекты. Сложность этого «разума» зависит от того, какую разновидность существ мы конструируем. К примеру, создания из Рас Man большую часть своего времени тратят на преследование или убегание от вас. Будь мы теми людьми, что написали Рас Man, мы имели бы возможность загорать где-нибудь на Гавайях, но у нас, в таком случае уже был бы алгоритмический инструмент для осуществления этих преследований и убеганий.

С другой стороны, если бы мы создавали игры, подобные «Космическим Захватчикам», придание видимости интеллекта вряд ли потребовало бы более пары дюжин строк в программе. Псевдокод выглядел бы примерно так:

1.       Продолжать движение в том направлении, в котором вы двигаетесь (вправо или влево);

2.       Когда вы попадете на границу экрана, изменить направление и двигаться вдоль оси высоты;

3.       Перейти к п.1.

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


К примеру, Terminator Rampage имеет действительно несложную систему искусственного интеллекта, примерно того же уровня сложности, что и в Рас Man. Однако при наличии трехмерной графики вместе с изощренным звуком существа в ней кажутся вполне одушевленными.

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

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

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


Как организована эта книга


Эта книга посвящена технике разработки игр для ПК. Она не является учебником, в котором материал представлен последовательно — от простого к сложному, однако в построении книги заметна определенная система.

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

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

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

Четвертая глава, «Механизмы двухмерной графики», покажет вам основные приемы, используемые при разработке двухмерных игр типа Commander Keen, Вы изучите технику вращения, масштабирования, перемещения и отсечения объектов в двухмерных играх.

Глава пятая, «Секреты VGA-карт», посвящена работе с графикой VGA. Вы изучите 256-цветный режим, таблицу цветов, копирование битовых изображений и т. д.

Шестая глава, «Третье измерение», укажет вам путь в трехмерный мир- Вы узнаете, как создавать DOOM-подобные миры, используя технику работы с трехмерной графикой.

Седьмая глава, «Усовершенствованная битовая графика и специальные эффекты», позволит вам получить представление об ускоренном выводе изобра­жений, работе со светотенью и другими специальными эффектами. Будьте внимательны — отсюда начинаются боевики!

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

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

В десятой главе, «Разработка музыки для компьютерных игр» вы изучите приемы работы с программами DIGPAK и MIDPAK, которые позволяют создавать впечатляющую музыку и звуковые эффекты.


Благодаря этим программам вы сможете добавлять оркестровую музыку и необычайные звуковые эффекты в ваши творения, что придаст им профессиональный вид. Эти программы использовались при создании таких популярных игр, как 7th Guest, Terminator 2029, Mechwarrior II.

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

Двенадцатая глава, «Мнимое время, прерывания и многозадачность», научит вас добавлять новое измерение — время. Вы изучите приемы параллельной обработки различных игровых задач, создания главного игрового цикла и как использовать прерывания при работе с устройствами ввода/вывода.

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

Четырнадцатая глава, «Связь», научит вас добавлять дополнительные возможности. Вы сможете звякнуть по модему приятелям и отправиться вместе с ними по неисследованным дебрям виртуальных миров. Игры, рассчитанные на нескольких участников, становятся все более популярными.

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

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

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

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

Девятнадцатая глава, "Игра Warlock (Колдун)", описывает игру, созданную автором этой книги.


Как работает джойстик


Теперь поговорим более подробно о том, как работает джойстик:

§

Каждая ось джойстика имеет связанный с ней потенциометр. Когда рукоятка отклоняется по оси Х или Y, то сопротивление соответствующего потенциометра изменяется;

§          Потенциометр, используется вместе с конденсатором для создания цепи нагрузки;

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

§          Напряжение снимается с конденсатора и сравнивается с эталонным. Когда напряжение достигает порогового значения, система выставляет флаг;

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

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

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



Калибровка джойстика


Теперь разберемся с калибровкой джойстика. Как я уже говорил ранее, резуль­тат, который мы получим от чтения джойстика в цикле, будет разным на разных ПК. На одних компьютерах значения по оси Х окажется в пределах от 0 до 255, на других - от 0 до 10000.

Таким образом, нам надо нормализовать эти данные или масштабировать их. Стандартным приемом в данных случаях может служить калибровка джой­стика самим игроком в setup'e игры. Во время этой процедуры игрок двигает джойстиком, а программа считывает и запоминает данные калибровки где-ни­будь на диске для дальнейшего использования.

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

§          Найти значения максимального и минимального отклонения по осям Х и Y;

§          Сохранить эту информацию;

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

Например, джойстик был откалиброван и мы обнаружим, что ось Х имеет значения от 0 до 255. Затем, если значение джойстика, например, по координате Х окажется равным 128, то можно с уверенностью сказать, что рукоятка находится в среднем положении (кстати, в процессе калибровки средняя пози­ция также запоминается).

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

Я думаю, что мы уже сказали о джойстиках все. Теперь напишем небольшую программку, в, которой используем функции из Листингов 3.1 и 3.2. При старте она просит игрока:

§          Подвигать джойстиком;

§          Установить джойстик в среднее положение;



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


§          Понажимать на кнопки.

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

Листинг 3.3. Программа работы с джойстиком (JOY.C).

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

#include <dos.h>

#include <bios.h>

#include <stdio.h>

#include <math.h>

#include <conio.h>

#include <graph.h>

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

#define JOYPORT      0х201 // порт джойстика - 201h

#define BUTTON_1_A   0х10 // джойстик А, кнопка 1

#define BUTTON_1 В   0х20 // джойстик А, кнопка 2

#define BUTTON_2_A   0х40 // джойстик В, кнопка 1

#define BUTTON_2_B   0х80 // джойстик В, кнопка 2

#define JOYSTICK_1_X 0х01 // джойстик А, ось Х

#define JOYSTICK_1_Y 0х02 // джойстик А, ось Y

#define JOYSTICK_2_X 0х04 // джойстик В, ось Х

#define JOYSTICK_2_Y 0х08 // джойстик В, ось Y

#define JOY_1_CAL       1 // команда калибровки джойстика А

#define JOY_2_CAL       2 // команда калибровки джойстика В

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

unsigned int

joy_1_max_x, // глобальные переменные для сохранения

joy_1_max_y, // калибровочных значений

joy_1_min_x,

joy_l_min_y,

joy_1_cx, joy_1_cy,

joy_2_max_x, joy_2_max_y,

joy_2_min_x, joy_2_min_y,

joy_2_cx, joy_2_cy;

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

unsigned char Buttons(unsigned char button)

{

// функция читает статус кнопок джойстика

outp(JOYPORT,0); // получаем запрос на получение статуса кнопок

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

return (~inp(JOYPORT) & button);

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

unsigned int Joystick(unsigned char stick)

{

// Функция  читает положение счетчика на основании количества

// циклов, прошедших между сбросом и установкой бита в порту

// джойстика. Встроенный ассемблер - прекрасная вещь!

_asm{

cli                      ; запрещаем прерывания


Клавиатура


Клавиатура - это наиболее сложное устройство ввода, которое есть в ПК. Она даже имеет свою собственную микросхему - контроллер ввода. Я провел много бессонных ночей, вчитываясь в листинги BIOS и пытаясь понять тайны, скрытые в работе с клавиатурой.

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

Для наших целей (для написания видеоигр) мы должны научиться хорошо работать с клавиатурой. Для этого вовсе не стоит разбираться с прерываниями, регистрами и портами. Мы будем использовать функции языка Си и BIOS для работы с очередью клавиатуры. Говоря о Си, я не имею в виду функции типа getch () и scanf (). Речь пойдет, скорее, о функциях типа _bios_keyboard ().

Примечание

Давайте приостановимся и немного подумаем, Общее правило для авторов игр - никогда не использовать BIOS. Верно? Хорошо, на самом деле BIOS вполне можно использовать для файловых операций и для выделения памяти. В общем, обращения к BIOS вполне допустимы в функциях, некритичных по времени. Попытка исполь­зовать его для работы с джойстиком или клавиатурой не будет для нас смертельна (в отличие от попыток организовать через BIOS вывод графики). Как я уже говорил, современные компьютеры достаточно быстры, чтобы нам не приходилось оптимизировать каждую запятую в тексте программы или писать ее целиком на ассемблере.

BIOS поддерживает несколько функций, которые мы будем использовать и которые приведены в таблице 3-1.

Таблица 3.1. Клавиатурные функции BIOS.

Bios INT 16h

Функция 00h - чтение символа с клавиатуры.

Вход:     АН: 00h

Выход:  АН - скан код

AL - ASCII-символ

Функция 01h - чтение статуса клавиатуры.

Вход:     АН: 01h

Выход:  АН - скан-код

AL - ASCII-символ

флаг Z: если 0, то в буфере есть символ, если 1 - нет символа.

Функция 02h - Флаги, возвращаемые клавиатурой.

Вход:     АН: 02h

Выход:  AL - байт статуса клавиатуры:

бит 0 - нажат правый

Shift;

бит 1 - нажат левый Shift;

бит 2 - нажата клавиша Ctrl;

бит 3 - нажата клавиша Alt;

бит 4 - Scroll Lock в положении ON;

бит 5 - Num Lock в положении ON;

бит 6 - Caps Lock в положении ON;

бит 7 - Insert в положении ON.



Кнопки джойстика


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

Есть джойстики с множеством кнопок, например, Trustmaster. Несмотря ни на что, его работа ничем не отличается от других джойстиков, а состояние лишних кнопок можно прочитать через другие порты ввода/вывода. Эти порты обычно указываются производителем в соответствующей документации. Обычно, порт 201h — это то окно, через которое можно общаться с джойстиком. Как показано на рисунке 3.2, этот порт связан и с джойстиком А, и с джойстиком В.

Мы узнаем назначение битов 0-3 чуть позже — в разделе «Чтение позиции джойстика». Биты 4-7 предназначены для чтения кнопок джойстика. Когда одна из кнопок нажата, то соответствующий бит порта 201h изменяется. Есть только одна маленькая деталь: значения битов всегда инвертированы. Это значит, что если вы, например, жмете кнопку 1 на джойстике А, то бит 0 изменит значение с \ на 0. Но в общем, это не особенно принципиально.

Листинг 3.1 содержит программный код чтения кнопки.

Листинг 3.1. Чтение кнопок джойстика.

#define JOYPORT      0х201 // порт джойстика = 201h

#define BUTTON_1_A   0х10   // джойстик А, кнопка 1

#define BUTTON_1_B   0х20  // джойстик А, кнопка 2

#define BUTTON_2_A  0х40   // джойстик В, кнопка 1

#define BUTTON_2_B   0х80   // джойстик В, кнопка 2

#define JOYSTICK_1_X 0х01   // джойстик А, ось ,Х

#define JOYSTICK_1_Y 0х02   // джойстик А, ось Y

#define JOYSTICK_2_X 0х04   // джойстик В, ось Х

#define JOYSTICK_2_Y 0х08   // джойстик В, ось Y

#define JOY_1_CAL 1      // эта команда калибрует джойстик А

#define JOY_2_CAL 2       // эта команда калибрует джойстик В

unsigned char Buttons(unsigned char button)

(

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

// сбрасываем содержимое порта 201h

outp (JOYPORT, 0);

// инвертируем прочитанное из порта значение и комбинируем




// его

с маской

return (~inp( JOYPORT) & button) ;

}

unsigned char Buttons_Bios (unsigned char button)

{ // чтение кнопок через обращение к BIOS

union _REGS inregs, outregs;

inregs.h.ah == 0х84;    // функция джойстика 84h

inregs.x.dx = 0х00;    // подфункция 0h - чтение кнопок

// вызов BIOS

_int86 (0х15, &inregs, &outr.egs);

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

return(~outregs.h.al) & button);

}

Теперь посмотрим на детали Листинга 3.1.

§          Функция Buttons() и Buttons_Bios() возвращают одинаковый результат. Buttons() посылает 0 в порт джойстика (это делается для того, чтобы инициировать порт) и затем читает данные;

§          Как только данные получены, мы маскируем младшие четыре бита и инвертируем четыре старших;

§          Этот листинг включает также определение констант (#define), что делает интерфейс более удобным;

§          Buttons_Bios() для чтения джойстика использует BIOS. Как только выполняется вызов, результат помещается в регистр AL. В принципе, для таких простых вещей, как кнопки, я использую прямой доступ к портам. Я уже говорил, что использование функций BIOS более медлительно. Правда, по отношению к джойстику это, может быть, и не самый плохой подход. Если вы хотите читать с помощью BIOS - читайте.


Кодирование прозрачности


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

Еще одно возможное решение данной проблемы заключается в применении к данным спрайта группового кодирования (run-lenght encoded, RLE), после которого последовательности «прозрачных» пикселей оказываются сгруппированными, как показано на рисунке 7.2.

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

§

Убедитесь, что данные спрайта записаны в формате:

столбец 1, столбец 2, ..., столбец N

где каждый из столбцов состоит из «прозрачных» и «непрозрачных» пикселей;

§          Чтобы обозначить каждый из типов пикселей, задайте флаг, например, 255 и 254; 

§          Следующий за флагом элемент данных должен определять длину последовательиости в байтах;                                          

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

Декодировать столбец данных можно будет по такому алгоритму:

ЕСЛИ  

255: непрозрачный

254: прозрачный

тогда

(255 | 254) , количество байт данных, данные




Этот шаблон будет повторяться много раз. Например, первый столбец на рисунке 7.2 будет кодироваться так:

254, 7, 0, 0, 0, 0, 0, 0, 0, 255, 2, 50, 50, 254, 1,

0

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

Затем можно провести дополнительную оптимизацию. Мы знаем, что «прозрачным» пикселям соответствуют нулевые значения данных (или любой другой индекс цвета, представляющий «прозрачность»), так зачем тратить место на избыточные нули 8 группах данных спрайта? Мы их можем отбросить. Новый формат будет содержать «непрозрачные» данные, флаги и значения длин последовательностей. В этой схеме возникает лишь одна небольшая проблема: мы не сможем использовать числа 254 и 255 как индекс цвета, поскольку эти значения уже использованы в качестве флагов контроля. (Однако я не думаю, что отсутствие двух цветов из 256 смертельно, не так ли?)

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

В этом разделе мы обсудили несколько идей улучшения бит-блиттинга и перемещения спрайтов. Я еще раз хотел бы подчеркнуть, что все сказанное — это только идеи. Я просто попытался продемонстрировать вам новый взгляд на данную проблему. Теперь вы знаете несколько путей ускорения процесса бит-блиттинга, но, по мере того как вы будете набираться опыта и знаний, у вас будут появляться все новые и новые пути. (Я знаю в 20 раз более быстрый способ осуществления бит-блиттинга, чем тот который мы здесь обсуждали, однако он применяется только в специальных случаях и основан на таком алгоритме оптимизации, что я потратил кучу времени, чтобы разобраться с ним — но все же я его победил!) Таким образом, существует далеко не один вариант ускорения блиттинга.И для того чтобы достигнуть уровня игр типа Wolfenstein или DOOM, вы должны быть очень искусны и изобретательны в программировании


Когда?


Музыка и звук должны использоваться не только в играх.

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

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

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

§          Для создания новых эмоций. Если вам действительно необходимо привлечь внимание пользователя, представьте себе, насколько звук клаксона будет эффективнее слова «ВНИМАНИЕ» в диалоговом окне. Писк при нажатии клавиши тоже для многих может сделать жизнь комфортнее. Если интересной графикой вы можете развеять скуку и пробудить интерес, то музыка может многократно усилить эти эмоции;  

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

Кроме того, не стоит использовать музыку и звук, если:           

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

§          Они являются просто фоном.

Все это приводит нас к следующему вопросу...



Когда объекты сталкиваются


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

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

Контролировать столкновения между прямоугольниками намного проще. Например, пусть даны прямоугольники R1 и R2. Алгоритм 11.1 контролирует их столкновение.

Алгоритм 11.1. Контроль столкновений с помощью описанных прямоугольников.

For (для каждой вершины прямоугольника R1) do

if (проверить координаты X и У для вершин прямоугольника R2)

{

есть столкновение

выход

} } // конец

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

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

• Вначале для выявления возможного столкновения использовать описанные прямоугольники по Алгоритму 11.1;

• Затем делать более тонкую проверку вероятных столкновений, отобранных после первого теста, уже с учетом истинной геометрической формы объекта.

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

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




Определение столкновений в «клеточном» пространстве тривиально. Все, что надо сделать, это определить, в какую клетку перемещается игрок и осмотреть, не занята ли она уже каким-либо объектом. Например, в рассмотренном выше примере пространство имело размер 10х10 клеток, а каждая клетка -16х16 пикселей. Общий размер изображения на экране составлял 160 пикселей. Если координаты игрока принимают значение (50,92), то он находится внутри какой-то клетки. Если же внутри этой клетки уже что-то есть, о наш игрок не должен в нее попадать!

Для расчета местоположения игрока мы делим значения координат на 16 Для координат (50,92) мы получим третью сверху, пятую слева клетку решетки Теперь мы можем посмотреть, что находится в этой клетке. Если там уже что-то есть, нужно отправить играющего назад, устроить ему неприятность либо наоборот, доставить удовольствие в зависимости от того, с каким объектом он столкнулся.

Вы можете использовать эту технику и в трехмерном пространстве. В играх типа Wolfenstein игрок всегда находится на одной и той же высоте, поэтому нам нужно отслеживать только координаты Х и Y. В играх типа DOOM задача усложняется, так как игрок может перемещаться и по координате Z (подниматься или опускаться по ступенькам или на лифтах).


Коммуникационная программа: NLINK


Программа NLINK завершает наш извилистый путь освоения последовательных коммуникаций для ПК. Я написал эту небольшую коммуникационную програм­мку, чтобы вы могли лучше оценить пройденное. Она соединяет два ПК через СОМ1 или COM2 и позволяет двум игрокам общаться по нуль-модемному кабелю. Для выхода из программы надо нажать клавишу Esc. Листинг 14.4 содержит законченную коммуникационную библиотеку и главную часть программы NLINK.

Листинг 14.4. Коммуникационная программа NLINK (NLINK.C).

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

#include <dos.h>

#include <bios.h>

#include <stdio.h>

#include <math.h>

#include <conio.h>

#include <graph.h>

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

// регистры UART

#define SER_RBF        0    // буфер чтения

#define SER_THR        0    // буфер записи

#define SER_IER        1    // регистр разрешения прерываний

#define SER_IIR        2    // регистр идентификации прерывания

#define SER_LCR        3    // регистр управляющих данных

// и разрешения загрузки делителя

#define SER_MCR        4    // регистр управления модемом

#define SER_LSR        5    // регистр состояния линии

#define SER_MSR        6    // регистр состояния модема

#define SER_DLL        0    // младший байт делителя

#define SER_DLH        1    // старший байт делителя

// битовые маски для управляющих регистров

#define SER_BAUD_1200 96    // значения делителя

// для скоростей 1200-19200 бод

#define SER_BAUD_2400 48

#define SER_BAUD_9600 12

#define SER_BAUD_19200 6

#define SER_GP02       8     // разрешение прерываний

#define COM_1          0х3F8 // базовый адрес регистров СОМ1

#define COM_2          Ox2F8 // базовый адрес регистров COM2

#define SER_STOP_1      0     //1 стоп-бит на символ

#define SER_STOP_2      4     //2 стоп-бита на символ

#define SER_BITS_5     0     //5 значащих бит на символ

#define SER_BITS 6     1     //6 значащих бит на символ

#define SER_BITS_7     2     //7 значащих бит на символ




#define SER_BITS 8     3     //8 значащих бит на символ

#define SER_PARITY_NONE 0    // нет контроля четности

#define SER_PARITY_ODD   8 // контроль по нечетности

#define SER PARITY EVEN 24    // контроль по четности

#define SER_DIV_LATCH_ON 128 // используется при загрузке делителя

#define PIC_IMR        0х21   // маска для регистра прерываний

#define PIC ICR        0х20   // маска для контроллера

// прерываний (порт 20h)

#define INT_SER_PORT_0 0x0C   // маска для управления

                       // прерываниями СОМ1 и COM3

#define INT_SER_PORT_1 0x0B   // маска для управления

// прерываниями COM2 и COM4

#define SERIAL_BUFF_SI2E 128 // размер буфера

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

void ( _interrupt _far *01d_Isr) (); // адрес старой подпрограммы

// обслуживания прерываний

// СОМ-порта

char ser_buffer[SERIAL_BUFF_SIZE];// буфер для приходящих символов

int ser_end = -1,ser_start=-l;      // указатели позиции в буфере

int ser_ch, char_ready=0;           // текущий символ и флаг

// готовности

int old_int_mask;                   // старое значение маски

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

int open_port;                      // текущий открытый порт

int serial_lock = ,0;                // "семафор" для процедуры

// обработки прерывания,

// управляющий записью

// в программный буфер

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

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,Ox20);

// Разрешаем работу с буфером

serial_lock = 0;

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

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

int Ready_Serial()

{

// функция возвращает значение, отличное от нуля,

// если есть в буфере есть символы и 0 - в противном случае

return(char_ready);

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

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

int Serial_Read() {

// функция возвращает последний записанный

//в программный буфер символ

int ch;

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

while(serial_lock){}

// проверяем/ есть ли в символы в буфере

if (ser_end != ser_start)

{

// изменяем значение начальной позиции буфера

if (++ser_start > SERIAL_BUFF_SIZE-1) ser_start = 0;

// читаем символ

ch = ser_buffer[ser_start];

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

if (char_ready > 0) --char ready;

// возвращаем символ вызвавшей функции

return(ch);

// конец действий, если буфер не пуст

else

// буфер был пуст - возвращаем 0

return(0) ;

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

Serial_Write(char ch)

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

// но вначале она ожидает, пока он освободится.

// Примечание: эта функция не связана с прерываниями-

// и запрещает их на время работы

// ждем освобождения буфера

while(!(_inp(open_port + SER_LSR) 5 0х20)){}

// запрещаем прерывания

_asm cli

// записываем символ в порт

_outp(open_port + SER_THR, ch);

// снова разрешаем прерывания

_asm sti

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

Open_Serial(int port_base, int baud, int configuration)

{

// Функция открывает последовательный порт, устанавливает его

// конфигурацию и разрешает прерывания при получении символа

// запоминаем базовый адрес порта

open_port = port_base;

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

// разрешаем загрузку делителя



_outp(port_base + SER_LCR, SER_DIV_LATCH_ON);

// посылаем младший и старший байты делителя

_outp(port_base + SER_DLL, baud);

_outp(port_base + ser_dlh, 0) ;

// устанавливаем конфигурацию порта

_outp(port_base + SER_LCR, configuration);

// разрешаем прерывания

_outp(port_base + SER_MCR, SER_GP02);

_outp(port_base + SER_IER, 1);

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

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

if (port_base == СОМ_1)

{

Old_Isr = _dos_getvect(INT_SER_PORT 0);,

_dos_setvect(INT_SER_PORT_0, Serial_Isr) ;

printf("\n0pening Communications Channel Com Port #1...\n");

}

else

{

Old_Isr = _dos_getvect(INT_SER_PORT_1);

_dos_setvect(INT_SER_PORT_1, Serial_Isr) ;

printf("\n0pening Communications Channel Com Port #2...\n");

}

// разрешаем прерывание СОМ-порта на уровне контроллера прерываний

old_int_mask = _inp(PIC_IMR);

_outp(PIC_lMR, (port_base==COM_l) ? (old_int_mask & OxEF):(old_int_mask & OxF7 ) );                                

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

Close_Serial (int port_base)

{                                   

// функция закрывает СОМ-порт, запрещает вызов его прерываний

// и восстанавливает старый обработчик прерывания           

// запрещаем прерывания по событиям СОМ-порта

_outp(port_base + SER_MCR, 0) ;

_outp(port_base + SER_IER, 0).;

_outp(PIC_IMR, old_int_mask );

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

if (port_base == СОМ_1)

{

_dos_setvect(INT_SER_PORT_0, 01d_Isr) ;

printf("\nClosing Conuflunications Channel Corn Port #1.\n");

}

else

{

_dos_setvect(INT_SER_PORT_l, 0ld_Isr);

printf("\nClosing Communications Channel Com Port #2.\n");

}

// конец функции // ОСНОВНАЯ ФУНКЦИЯ /////////////////////////////

main ()

{

char ch;

int done=0;

printf("\nNull Modem Terminal Communications Program.\n\n");

// открываем СОМ1

Open_Serial(COM_1,SER_BAUD_9600,



SER_PARITY_NONE | SER_BITS_8 | SER_STOP_1);

// главный рабочий цикл

while (!done) {

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

if (kbhit()) {

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

ch = getch(); printf("%c",ch);

// посылаем символ на удаленную машину

Serial_Write(ch) ;

// не была ли нажата клавиша ESC? Если да - конец работы

if (ch==27) done=l;

// Если был введен символ "перевод каретки" (CR),

// добавляем символ "новая строка" (LF)

if (ch==13)

{

printf("\n");

Serial_Write(10);

}

}// конец обработки клавиатуры

// пытаемся получить символ с удаленной машины

if (ch = Serial_Read()) printf("%c", ch);

if (ch == 27) { printf("\nRemote Machine Closing Connection.");

done=l;

} // конец обработки нажатия ESC на удаленной машине

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

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

Close_Serial(COM_l);

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

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


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


Если вы не играли в свою любимую аркадную игру и не убивали кучу монстров уже несколько месяцев — вы явно заработались! Вернитесь к играм и увидите, как вы помолодеете и как разгладятся морщины на вашем утомленном лице. Дни Hunt the Wampus и Lunar Lander миновали, и теперь игры стали намного ярче, красочней и хитрее.

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

Графика DOOM дает вам полное ощущение пространства и перспективы — как если бы вы действительно находились в центре событий. Для этого DOOM использует приемы работы с трехмерной графикой. Разработчики игр для ПК используют трехмерную графику для увеличения реалистичности игры любого типа — посмотрите на 7th Guest, MYST, X-Wing, Outpost, Indy Car Racing. Часто трехмерная графика применяется и в имитаторах спортивных игр, таких как скачки или бокс.

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



Компонент нормализации вершины


Я совсем упустил одну маленькую деталь, которую вы уже, наверное, заметили. «Как мы можем умножить вершину на матрицу размером 3х3?» Неплохой вопрос. Рад, что вы спросили. Чтобы выполнить это, мы должны изменить представление структуры вершин, добавив компонент нормализации. Компонент нормализации - это просто единица, добавленная в конец структуры, описывающей каждую вершину. Для этого нам надо чуть-чуть изменить исходные тексты в Листинге 4,4, в котором описаны вершины. Все это отражено в структуре данных в Листинге 4.10.

Листинг 4.10. Новая структура данных для вершин.

#define X_COMP 0

#define Y_COMP 1

#define N_COMP 2

typedef struct vertex_typ

{

float p[3];

) vertex, *vertex_ptr;

Как вы можете заметить, теперь мы превратили матрицу Р, списывающую вершину, в массив. Наша матрица преобразований имеет размер 3х3, и для умножения нам надо иметь матрицу размером 1х3. Матрица Р удовлетворяет этому условию.



Конечные автоматы


Конечные автоматы (КА) - близкие друзья самых разнообразных механизмов. пни царствуют в виртуальном пространстве компьютеров. КА используются в разработке аппаратного и программного обеспечения. Поскольку мы коснулись этой темы, скажу, что КА - это программно моделируемая машина, которая движется от состояния к состоянию, основываясь на входных данных. Кроме того КА обычно запоминает все состояния, в которых он побывал. Посмотрите на рисунок 13.3, на котором изображена диаграмма простейшего КА. (Что именно этот КА делает - не имеет никакого значения).

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

Теперь у нас есть набор решений, чтобы ответить на вопросы:

§          Какая причина заставит КА изменить свое состояние?

§          Как заставить КА выбрать следующее состояние?

Это хорошие вопросы - посмотрим, как выглядят видеоигры изнутри. Взгляните на рисунок 13.4. На нем показано абстрактное представление некоторого КА, правда, пока еще без логики смены состояний.



Конечный автомат, управляемый окружающей средой


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

Начнем создавать КА с того, что он будет случайным образом выбирать одно из нижеперечисленных состояний:

§          преследование;

§          уклонение;

§          случайное;

§          шаблон.

Но в настоящей игре для управления состоянием КА вместо случайных чисел было бы лучше привлечь саму игровую ситуацию. Ниже приведен пример КА действующего по обстоятельствам:

§          Если игрок близко, КА переключается в состояние Шаблон;

§          Если игрок далеко, можно заставить ПК охотиться за ним, используя состояние Преследование;

§          Если игрок обрушил на наше маленькое создание шквальный огонь, КА моментально переходит в состояние Уклонение;

§          Наконец, при любой иной ситуации КА отрабатывает состояние Случайное. Рисунок 13.5 демонстрирует подобное поведение конечного автомата.

Перейдем к программной реализации описанного КА. Мы не станем создавать сложное игровое пространство. Для демонстрации нам достаточно иметь чистый экран с парой точек, изображающих игрока и противника. Чтобы понять суть, этого вполне достаточно. Программа из Листинга 13.4 моделирует полет действительно назойливой мухи, которая буквально заедает игрока.

Листинг 13.4. Умная «Муха» (BFLY.C).

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

#include <stdio.h>

#include <graph.h>

#include <math.h>




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

#define STATE_CHASE    1

#define STATE_RANDOM   2

#define STATE_EVADE    3

#define STATE_PATTERN 4

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

// Указатель на системную переменную, содержащую значение таймера.

// Содержимое этой 32-битовой ячейки обновляется 18.2 раза

//в секунду                  

unsigned int far *clock=(unsigned int far *)0х0000046C;

// Х- и Y-компоненты шаблонов траекторий, по которым

// будет двигаться "Муха"

int patterns_x[33[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,-1,-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,-l,-l,-l,-l,-l, 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:046Ch

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,    // номер текущего шаблона

pattern element,     // текущая команда шаблона

select_state=0,          // флаг необходимости смены состояния

clicks=20,               // количество циклов, в течение которых



// сохраняется текущее состояние

fly_state = STATE CHASE; // "Муха" начинает с преследования

float distance;          // используется при расчете

// расстояния между игроком и "Мухой"

_setvideomode(_MRES256COLOR);

printf("    Brainy 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=l ;

} break;

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

}

// конец обработки нажатия клавиши

// теперь перемещаем противника

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

// определяем текущее состояние КА

switch(fly_state)

{

case STATE_CHASE:

{

_settextposition(24,2);

printf("current state:chase   "};.

// реализуем Алгоритм Преследования

if (px>ex) ex++;

if (px<ex) ex--;

if (py>ey) ey++;

if (py<ey) ey--;

// не пора ли перейти к другому состоянию?

if (--clicks==0) select_state=l;

} break;

case STATE_RANDOM:

(

_settextposition(24,2} ;

printf("current state:random   ") ;

// перемещаем "Муху" в случайном направлении

ex+=curr_xv;

ey+=curr_yv;

//не пора ли перейти к другому состоянию?

if (--clicks=0) select_state=l;

} break;

case STATE_EVADE:

{

_settextposition(24,2) ;

printf("current state:evade   ");

// реализуем Алгоритм Уклонения

if (px>ex) ex--;

if (px<ex) ex++;

if (py>ey) ey—;

if (py<ey) ey++;

//не пора ли перейти к другому состоянию?

if (--clicks==0) select_state=l;

} break;

case STATE_PATTERN:

{

_settextposition(24,2);

printf("current state:pattern   ");

// перемещаем "Муху", используя следующий

// элемент текущего шаблона

ex+=patterns_x[current_pattern][pattern_element];

ey+=patterns_y[current_pattern][pattern_element];



// обработка шаблона закончена?

if (++pattern element==20)

{

pattern_element = 0;

select_state=l;

} // конец проверки завершения шаблона

} break;

default;break;

} // конец обработки текущего состояния

// надо ли менять, состояние?

if (select_state==l)

{

// Выбор нового состояния основан на

// игровой ситуации и неявной логике.

// Для выбора используется расстояние до игрока

distance = sqrt(.5+fabs((рх-ех)*(рх-ех)+(ру-еу)*(ру-еу))) ;

if (distance>5&&distance<15&&rand()%2==1)

{

// выбираем новый шаблон

current_pattern = rand()%3;

// переводим "мозг" в состояние действий по шаблону

fly_state = STATE_PATTERN;

pattern_element = 0;

} // конец обработки ситуации "близко к игроку"

else if (distance<10) // слишком близко, убегаем!

clicks=20;                                  

fly_state = STATE_EVADE;

} // конец обработки ситуации "слишком близко"

else

if (distance>25&sdistance<100&&rand()%3==1)

{

// преследуем игрока clicks=15;

fly_state = STATE_CHASE;

} // конец обработки ситуации Преследование

else

if (distance>30&&rand()%2==l)

{

clicks=10;

fly_State = STATE_RANDOM;

curr_xv = -5 + rand()%10; //от -5 до +5

curr_yv = -5 + rand()%10; //от -5 до +5

} // конец "неявной логики"

else

{

clicks=5;

fly_state = STATE_RANDOM;

curr_xv = -5 + rand()%10; //от -5 до +5

curr_yv = -5 + rand()%10; //от -5 до +5

} // конец оператора else // сбрасываем флаг смены состояния

select_state=0;

} // конец Алгоритма Смены Состояния

// Убеждаемся, что "Муха" находится в пределах экрана

if (ex>319) ех=0;

i£ (ex<0)   ех=319;

if (ey>199) еу=0;

if (ey<0)   еу=199;

// конец работы "мозга"

_setcolor(12);

_setpixel(ex,ey);

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

Timer(1);

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

_setvideomode(_DEFAULTMODE) ;

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

Запустив программу, вы наверняка обратите внимание на кажущуюся сложность поведения «Мухи».А ведь для получения такого результата использована весьма простая схема! Возможно, и вы заметите то, что увидел я - чуть ли не человеческую способность мыслить.