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

«Прозрачные» пиксели


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

Листинг 17.3 содержит новую функцию, называемую TransparentBlt(). Она заменит нам OpaqueBIt(). Разница между ними состоит только в том, что TransparentBlt() пропускает «прозрачные» пиксели (и это тоже тормозит работу программы).

Но как же TransparentBlt() отличает «прозрачные» пиксели от «непрозрачных»? Я решил, что любой пиксель со значением цвета, равным 0 (обычно, это черный) будет «прозрачным», но вы можете назначить для этого другой цвет. Функция пропускает любой пиксель, у которого значение цвета равно объявленной константе TRANSPARENT. Программа из Листинга 17.3 (PARAL1.C) является демонстрацией смещения двух повторяющихся слоев. Дальний слой сплошной, в то время как ближний включает в себя «прозрачные» пиксели. Для вывода изображений используются функции OpaqueBIt() и TransparentBit() соответственно. Несмотря на то, что у нас имеется всего два движущихся слоя, эффект получается довольно реалистичным. Как и в программе из Листинга 17.2, курсорные клавиши «влево» и «вправо» перемещают изображение по горизонтали, а для завершения программы нужно нажать Esc.

Обратите внимание, что скорость смены кадров в этой программе значительно ниже, чем в предыдущей. Это происходит из-за использования функции для работы с «прозрачными» пикселями. На компьютере с процессором 386SX/25 я получил примерно 10 кадров в секунду.


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

Листинг 17.3. Простой двойной параллакс (PARAL1.C).

#include <stdio.h>

#include <stdlib.h>

#include <string.h>

#include <time.h>

#include <dos.h>



#include "paral.h"

char *MemBuf,            // указатель на дублирующий буфер

*BackGroundBmp,     // указатель на битовую карту фона

*ForeGroundBmp,     // указатель на битовую карту

// ближнего плана

*VideoRam;          // указатель на видеобуфер

PcxFile pcx;             // структура данных

// для чтения PCX-файла

int volatile KeyScan;    // заполняется обработчиком

// прерывания клавиатуры

int frames=0,            // количество нарисованных кадров

PrevMode;            // исходный видеорежим

int background, // позиция прокрутки фона

foreground, //позиция прокрутки битовой карты

// ближнего плана position;    // общее расстояние прокрутки

void _interrupt (*OldInt9)(void); // указатель на обработчик

// прерывания клавиатуры BIOS

// Функция загружает 256 - цветный PCX-файл

int ReadPcxFile(char *filename,PcxFile *pcx)

{

long i;

int mode=NORMAL,nbytes;

char abyte,*p;

FILE *f;

f=fopen(filename,"rb");

if(f==NULL)

return FCX_NOFILE;

fread(&pcx->hdr,sizeof(PcxHeader),1, f);

pcx_width=1+pcx->hdr.xmax-pcx->hdr.xmin;

pcx->height=1+pcx->hdr.ymax-pcx->hdr.ymin;

pcx->imagebytes=(unsigned int) (pcx->width*pcx->height);

if(pcx->imagebytes > PCX_MAX_SIZE) return PCX_TOOBIG;

pcx->bitmap= (char*)malloc (pcx->imagebytes);

if(pcx->bitmap == NULL) return PCX_NOMEM;

p=pcx->bitmap;

for(i=0;i<pcx->imagebytes;i++)

{

if(mode == NORMAL)

{

abyte=fgetc(f);

if((unsigned char)abyte > 0xbf)

{ nbytes=abyte & 0x3f;

abyte=fgetc(f);

if(--nbytes > 0)

mode=RLE;

}

}

else if(-—nbytes == 0) mode=NORMAL;

*p++=abyte;

}

fseek(f,-768L,SEEK_END);      // получить палитру,из PCX-файла

fread(pcx->pal,768,1,f);

p=pcx->pal;



for(i=0;i<768;i++) // битовый сдвиг цветов в палитре

*р++=*р >>2;

fclose(f) ;

return PCX_OK;

}

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

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

// если стандартный обработчик прерывания 9h не будет заблокирован

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

// к переполнению буфера клавиатуры и появлению крайне неприятного

// звука из динамика.

void _interrupt Newlnt9(void)

{

register char x;

KeyScan=inp(0х60);// прочитать код клавиши

x=inp(0x61);      // сообщить клавиатуре, что символ обработан

outp(0x61, (х|0х80));

outp(0х61,х);

outp(0х20,0х20);  // сообщить о завершении прерывания

if(KeyScan == RIGHT_ARROW_REL ||// проверка кода клавиши

KeyScan == LEFT_ARROW_REL) KeyScan=0;

}

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

void RestoreKeyboard(void)

{

_dos_setvect(KEYBOARD,OldInt9);   // восстанавливаем

// обработчик BIOS

}

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

void InitKeyboard(void)

{

OldInt9= _dos_getvect(KEYBOARD);   // сохраняем адрес

//  обработчика BIOS

_dos_setvect(KEYBOARD,NewInt9);   // устанавливаем новый

//  обработчик прерывания 9h

}

// Эта функция использует функции BIOS для установки в регистрах

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

// определяемыми массивом раl[]

void SetAllRgbPalette(char *pal)

{

struct SREGS s;

union REGS r;

segread(&s);  // читаем текущее значение сегментных регистров

s.es=FP_SEG((void far*)pal);  // в ES загружаем сегмент ра1[]

r.x.dx=FP OFF((void far*}pal);// в DX загружаем смещение pal[]

r.x.ax=0xl012;         // готовимся к.вызову подфункции // 12h функции BIOS 10h

r.x.bx=0;            /;/ номер начального регистра палитры

r.х.сх=256;           // номер последнего изменяемого регистра

int86x(0xl0,&r,&r,&s);// вызов видео BIOS

}



// Функция устанавливает режим 13h

// Это MCGA-совместимыЙ режим 320х200х256 цветов

void InitVideo()

{

union REGS r;

r.h.ah=0x0f;       // функция Ofh - установка видеорежима

int86(0xl0,&r,&r); // вызов видео BIOS

PrevMode=r.h.al;   // сохраняем старое значение режима

r.x.ax=0xl3;       // устанавливаем режим 13h

int86(0х10,&r,sr); // вызов видео BIOS

VideoRam=MK_FP(0xa000,0); // создаем указатель на видеопамять

}

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

void RestoreVideo()

{

union REGS r;

r.x,ax=PrevMode;   //исходный видеорежим

int86(0х10,&r,&r); // вызов видео BIOS

}

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

int InitBitmaps()

{

int r;

// начальное положение линии деления

background=foreground=1;

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

r=ReadPcxFile("backgrnd.pcx",&pcx);

// проверка на ошибки чтения if(r != РСХ_ОК)

return FALSE;

// запоминаем указатель на битовую карту

BackGroundBmp=pcx.bitmap;

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

SetAllRgbPalette(pcx.pal) ;

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

r=ReadPcxFile("foregrnd.pcx",&pcx);

// проверка на ошибки чтения

if (r != РСХ_ОК) return FALSE;

//запоминаем указатель на битовую карту

ForeGroundBmp=pcx.bitmap;

// создаем буфер в памяти

MemBuf=malloc(MEMBLK);

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

if(MemBuf == NULL) return FALSE;

memset(MemBuf,0,MEMBLK); // очистка буфера

return TRUE;

// все в порядке!

}

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

void FreeMem()

(

free(MemBuf);

free(BackGroundBmp);

free(ForeGroundBmp);

}

// Функция рисует слои параллакса.

// Порядок отрисовки определяется координатой слоя по оси Z.

void DrawLayers()

{

OpaqueBlt(BackGroundBmp,0,100,background);

TransparentBlt(ForeGroundBmp,50,100,foreground);

}

// Эта функция осуществляет анимацию. Учтите, что это наиболее

// критичная по времени часть программы. Для оптимизации отрисовки

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

// следует переписать на ассемблере.


Как правило, это увеличивает

// быстродействие на 100 процентов.

void AnimLoop()

{

while(KeyScan != ESC_PRESSED)    // пока не нажата клавиша ESC

(

switch(KeyScan)     // определяем, какая клавиша была нажата

{

case RIGHT_ARROW_PRESSED:      //нажата "стрелка вправо"

position--;                  // изменяем позицию

if(position < 0)             // останавливаем прокрутку,

//  если дошли до конца

{

position=0;

break;

} backgrpund —=1;      // прокручиваем фон влево на 2 пикселя

if(background < 1)        // дошли до конца?

background+=VIEW_WIDTH; // ...если да - возврат к началу

foreground-=2;            // прокручиваем верхний

// слой влево на 4 пикселя

if(foreground < 1)        // дошли до конца?

foreground+=VIEW_WIDTH; // ...если да - возврат к началу

break;

case LEFT_ARROW_PRESSED:      // нажата "стрелка влево"

position++;          // изменяем текущую позицию прокрутки

if(position > TOTAL_SCROLL) // останавливаем прокрутку,

// если дошли до конца

{

position=TOTAL_SCROLL;

break;

}

background+=l;     // прокручиваем фон вправо на 2 пикселя

if(background > VIEW_WIDTH-1) // дошли до конца?

background-=VIEW_WIDTH; // ...если да - возврат к началу

foreground+=2;            // прокручиваем верхний слой

// вправо на 4 пикселя

if(foreground > VIEW_WIDTH-1) // дошли до конца?

foreground-=VIEW_WIDTH; // ...если да - возврат к началу

break;

default:                   // игнорируем остальные клавиши

break;

}

DrawLayers();                // рисуем слои в буфере в        

// оперативной памяти

memcpy(VideoRam,MemBuf,MEMBLK); // копируем буфер в

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

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

) }

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

void Initialize()

{

position=0;

InitVideo();            // устанавливаем видеорежим 13h

InitKeyboard();         // устанавливаем наш обработчик

// прерывания клавиатуры

if(!InitBitmaps())      // загружаем битовые карты



{

CleanUp();        //освобождаем память

printf("\nError loading bitmaps\n");

exit(1);

} }

// функция выполняет всю необходимую очистку

void Cleanup()

{

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

RestoreKeyboard();    // восстанавливаем обработчик

// прерывания клавиатуры BIOS

FreeMem();            // освобождаем всю выделенную память



// Это начало программы. Функция вызывает процедуры инициализации.

// Затем читает текущее значение системного таймера и запускает

// анимацию. Потом вновь читается значение системного таймера.

// Разница между исходным и конечным значениями таймера

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

int main()

{

clock_t begin,fini;

Initialize(};          // проводим инициализацию

begin=clock();         // получаем исходное значение таймера

AnimLoop();            // выполняем анимацию

fini=clock();          // получаем значение таймера

CleanUp();             // восстанавливаем измененные параметры

printf("Frames: %d\nfps: %f\n",frames,

(float)CLK_TCK*frames/(fini-begin));

return 0;

}


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