Графика для Windows средствами DirectDraw

       

Функция CreateAviSurface()


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


BOOL AviPlayWin::CreateAviSurface() { if (avisurf) avisurf->Release(), avisurf=0;

avisurf=CreateSurface( srcfmt->biWidth, srcfmt->biHeight );

CRect displayrect=GetDisplayRect();

x=(displayrect.Width()-srcfmt->biWidth)/2; y=0;

return TRUE; }


После освобождения поверхности, созданной ранее, функция CreateAviSurface() с помощью функции CreateSurface() интерфейса DirectDraw создает поверхность, размеры которой совпадают с размерами кадра. Кроме того, функция CreateAviSurface() инициализирует переменные x и y, определяющие положение поверхности AVI на вторичном буфере. В нашем случае кадры будут выравниваться по центру экрана, поэтому в вычислениях применяется функция DirectDrawWin::GetDisplayRect() для определения размеров экрана.



Функция CreatePalette()


Если второй аргумент функции CreateSurface() равен TRUE, CreatePalette() создает и заполняет объект DirectDrawPalette данными, полученными из BMP-файла. Функция CreatePalette() выглядит так:


BOOL DirectDrawWin::CreatePalette(RGBQUAD* quad, int ncolors) { if (palette) palette->Release(), palette=0; PALETTEENTRY pe[256]; ZeroMemory( pe, sizeof(pe) ); for( int i=0; i<ncolors; i++) { pe[i].peRed = quad[i].rgbRed; pe[i].peGreen = quad[i].rgbGreen; pe[i].peBlue = quad[i].rgbBlue; }

HRESULT r=ddraw2->CreatePalette( DDPCAPS_8BIT | DDPCAPS_ALLOW256, pe, &palette, 0 ); if (r!=DD_OK) { TRACE("failed to create DirectDraw palette\n"); return FALSE; }

primsurf->SetPalette( palette );

return TRUE; }


Палитры DirectDraw создаются функцией CreatePalette() интерфейса DirectDraw, которой передается массив структур PALETTEENTRY. Чтобы выполнить это требование, приходится преобразовывать массив структур RGBQUAD, извлеченный из BMP-файла, во временный массив (структуры PALETTEENTRY и RGBQUAD очень похожи, поэтому такое преобразование оказывается тривиальным). Затем созданный массив передается функции CreatePalette(). Флаг DDPCAPS_ALLOW256 сообщает, что мы намерены задать значения всех 256 элементов палитры. Если вы пропустили главу 4 (конечно же, нет!), вернитесь к ней и ознакомьтесь с возможными аспектами использования этого флага.

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



Функция CreateSurface()


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



Давайте посмотрим, как реализована функция CreateSurface() (см. листинг 5.1).

Листинг 5.1. Функция CreateSurface()


LPDIRECTDRAWSURFACE DirectDrawWin::CreateSurface(LPCTSTR filename, BOOL installpalette) { int imagew, imageh; GetBmpDimensions( filename, imagew, imageh ); LPDIRECTDRAWSURFACE surf=CreateSurface( imagew, imageh ); if (surf==0) { TRACE("CreateSurface(filename) failed to create surface\n"); return 0; }

ifstream bmp( filename, ios::binary | ios::nocreate ); if (!bmp.is_open()) { TRACE("LoadSurface: cannot open Bmp file\n"); return 0; }

BITMAPFILEHEADER bmpfilehdr; bmp.read( (char*)&bmpfilehdr, sizeof(bmpfilehdr) ); char* ptr=(char*)&bmpfilehdr.bfType; if (*ptr!='B' || *++ptr!='M') { TRACE("invalid bitmap\n"); return 0; }

BITMAPINFOHEADER bmpinfohdr; bmp.read( (char*)&bmpinfohdr, sizeof(bmpinfohdr) ); bmp.seekg( sizeof(bmpfilehdr)+bmpinfohdr.biSize, ios::beg ); int imagebitdepth=bmpinfohdr.biBitCount;

int imagesize=bmpinfohdr.biSizeImage; if (imagesize==0) imagesize=((imagew*(imagebitdepth/8)+3) & ~3)*imageh;

if (bmpinfohdr.biCompression!=BI_RGB) { TRACE("compressed BMP format\n"); return 0; } TRACE("loading '%s': width=%d height=%d depth=%d\n", filename, imagew, imageh, imagebitdepth); if (imagebitdepth==8) { int ncolors; if (bmpinfohdr.biClrUsed==0) ncolors=256; else ncolors=bmpinfohdr.biClrUsed;

RGBQUAD* quad=new RGBQUAD[ncolors]; bmp.read( (char*)quad, sizeof(RGBQUAD)*ncolors ); if (installpalette) CreatePalette( quad, ncolors ); delete [] quad; }

BYTE* buf=new BYTE[imagesize]; bmp.read( buf, imagesize );

if (!Copy_Bmp_Surface( surf, &bmpinfohdr, buf )) { TRACE("copy failed\n"); delete [] buf; surf->Release(); return 0; }

delete [] buf;

return surf; }

<
br> Сначала эта функция определяет размеры изображения из BMP-файла с помощью функции GetBmpDimensions() — простой функции класса DirectDrawWin, которая открывает BMP-файл и извлекает из заголовка ширину и высоту изображения. На основании полученных данных создается новая поверхность с использованием версии CreateSurface(), которая создает поверхность по ее размерам. Новая поверхность заполнена случайными пикселями, но мы не стираем ее, потому что вскоре значение каждого пикселя будет задано в соответствии с содержимым BMP-файла.

Затем мы открываем BMP-файл с помощью класса ifstream и извлекаем из него данные заголовка. Далее проверяется сигнатура файла; если проверка дает отрицательный результат, BMP-файл может содержать неверную информацию, поэтому функция завершает работу.

Дополнительные данные заголовка извлекаются с помощью структуры BITMAPINFOHEADER. Обратите внимание: после заполнения структуры текущая позиция в файле ifstream изменяется в соответствии со значением поля biSize. Это сделано для того, чтобы в будущем, при увеличении размера структуры BITMAPINFOHEADER, наша программа нормально работала с новыми BMP-файлами.

Ширина и высота изображения уже известны, поэтому читать значения полей biWidth и biHeight структуры BITMAPINFOHEADER не нужно. Функция CreateSurface() считывает глубину пикселей (biBitCount) и размер изображения (biSizeImage). Как упоминалось выше, поле biSizeImage часто равно нулю, поэтому мы проверяем его значение. Снова приведу соответствующий фрагмент кода:

int imagesize=bmpinfohdr.biSizeImage;if (imagesize==0) imagesize=((imagew*(imagebitdepth/8)+3) & ~3)*imageh;


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

И последняя проверка: по содержимому поля biCompression мы убеждаемся, что BMP-файл не содержит сжатых данных, не поддерживаемых нами.


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

Если изображение является палитровым, мы загружаем палитру. Количество элементов палитры в файле определяется полем biClrUsed, но это поле также может быть равно нулю. В этом случае предполагается, что присутствуют все 256 элементов палитры. Палитра загружается лишь в том случае, если параметр installpalette равен TRUE; тогда вызывается функция CreatePalette(). Вскоре мы рассмотрим код этой функции.

Следующий этап — чтение графических данных, которое в нашем случае выполняется одним вызовом функции ifstream::read(). Графические данные передаются функции Copy_Bmp_Surface(), отвечающей за пересылку данных новой поверхности. После возврата из функции Copy_Bmp_Surface() буфер с графическими данными освобождается. BMP-файл автоматически закрывается при возвращении из функции CreateSurface() (поскольку локальный объект ifstream выходит из области видимости).




Функция DrawScene()


Классы, производные от DirectDrawWin, реализуют функцию DrawScene(), в которой происходит обновление экрана. Версия DrawScene() из класса BounceWin выглядит так:


void BounceWin::DrawScene() { CRect limitrect=GetDisplayRect(); x+=xinc; y+=yinc; if (x<-160 || x>limitrect.right-160) { xinc=-xinc; x+=xinc; } if (y<-100 || y>limitrect.bottom-120) { yinc=-yinc; y+=yinc; } ClearSurface( backsurf, 0);

BltSurface( backsurf, surf1, x, y );

primsurf->Flip( 0, DDFLIP_WAIT ); }


Сначала функция GetDisplayRect() получает объект CRect, хранящий ширину и высоту текущего видеорежима. Эти размеры будут использоваться для ограничения перемещений растрового изображения в соответствии с видеорежимом. Далее вычисляются значения переменных x и y класса BounceWin, определяющих местонахождение растра на экране.

Затем мы вызываем функцию ClearSurface() и передаем ей два аргумента: указатель backsurf и 0. Это приводит к тому, что вторичный буфер заполняется черным цветом. Хотя я упоминал о том, что использование ClearSurface() иногда осложняется различными форматами пикселей, заполнение поверхностей черным работает надежно. Для палитровых поверхностей 0 означает черный цвет, потому что по умолчанию он стоит в палитре на первом месте; для беспалитровых поверхностей 0 всегда соответствует черному цвету.

Функция DrawScene() использует функцию DirectDrawWin::BltSurface() для копирования поверхности surf1 на поверхность backsurf. Два последних аргумента BltSurface() определяют точку поверхности-приемника, куда должно быть скопировано содержимое источника. Для выполнения этой операции можно было бы воспользоваться функцией Blt() или BltFast() интерфейса DirectDrawSurface, но мы не делаем этого из-за возможного отсечения. Обратите внимание - код, определяющий положение растра, позволяет источнику выйти за пределы приемника, в результате чего может потребоваться отсечение. Мы не можем воспользоваться функцией Blt(), потому что тогда потребовалось бы присоединить к приемнику объект DirectDrawClipper, чего мы не делаем. Функция BltFast() тоже не подходит, потому что она вообще не поддерживает отсечения. Функция BltSurface() автоматически выполняет отсечение, а функции Blt() и BltFast() вызываются внутри нее.

Но перед тем, как переходить к функции BltSurface(), мы закончим рассмотрение функции DrawScene(). Она завершается вызовом функции Flip(). При этом происходит переключение страниц, и подготовленный нами кадр отображается на экране. Функция Flip() получает два аргумента: указатель на поверхность и переменную типа DWORD, предназначенную для установки флагов. Указатель на поверхность необходим лишь в нестандартных ситуациях, когда в переключении поверхностей участвует несколько вторичных буферов. Второй аргумент обычно содержит флаг DDFLIP_WAIT, показывающий, что возврат из функции должен происходить только после того, как переключение страниц завершится.


Функция DrawScene() отвечает за подготовку нового кадра во вторичном буфере, обновление курсора и переключение страниц. Функция DrawScene()

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

Листинг 7.4. Функция DrawScene()


void CursorWin::DrawScene() { //------ Проверить клавишу ESCAPE ------- static char key[256]; keyboard->GetDeviceState( sizeof(key), &key ); if ( key[DIK_ESCAPE] & 0x80 ) PostMessage( WM_CLOSE );

//------ Обычные задачи ------ ClearSurface( backsurf, 0 );

BltSurface( backsurf, dm_surf, 539, 0 );

static coil_idx; BltSurface( backsurf, coil[coil_idx], coilx, coily ); coil_idx=(coil_idx+1)%coil_frames;

//------ Начало синхронизированной секции ------ critsection.Lock();

//------ Сохранить область вторичного буфера под курсором RECT src; src.left=curx; src.top=cury; src.right=curx+cursor_width; src.bottom=cury+cursor_height; cursor_under->BltFast( 0, 0, backsurf, &src, DDBLTFAST_WAIT );

//------ Нарисовать курсор во вторичном буфере backsurf->BltFast( curx, cury, cursor, 0, DDBLTFAST_SRCCOLORKEY | DDBLTFAST_WAIT );

primsurf->Flip( 0, DDFLIP_WAIT ); while (primsurf->GetFlipStatus(DDGFS_ISFLIPDONE)!=DD_OK) // ничего не делать (ждать, пока закончится // переключение страниц)

int x, y; BOOL newclick=FALSE; int count=mouseclickqueue.GetCount(); while (count--) { MouseClickData mc=mouseclickqueue.RemoveTail(); if (mc.button==0) { x=mc.x; y=mc.y; newclick=TRUE; } }

critsection.Unlock(); //------ Конец синхронизированной секции -------

//------ Сделать паузу в соответствии с выбранной задержкой ---- if ( delay_value[dm_index]!=0) Sleep( delay_value[dm_index] );

//------ Обновить меню задержки -------- if (newclick) { int max_index=sizeof(delay_value)/sizeof(int)-1; int menux=screen_width-dm_width+dm_margin; int menuw=dm_width-dm_margin*2; if (x>=menux && x<=menux+menuw) { int index=(y-dm_header)/dm_entrysize; if (index>=0 && index<=max_index && index!=dm_index) { dm_index=index; UpdateDelaySurface(); } } } }

<


Наконец, все готово к отображению кадров видеоролика. Для этого мы подготавливаем и выводим очередной кадр при каждом вызове функции DrawScene() классом DirectDrawWin. Функция DrawScene() выглядит так:


void AviPlayWin::DrawScene() { long r; r=AVIStreamRead( avistream, curframe, 1, rawdata, buflen, 0, 0 ); if (r) { TRACE("AVIStreamRead failed: "); switch (r) { case AVIERR_BUFFERTOOSMALL: TRACE("BUFFERTOOSMALL\n"); break; case AVIERR_MEMORY: TRACE("MEMORY\n"); break; case AVIERR_FILEREAD: TRACE("FILEREAD\n"); break; } }

r=ICDecompress( decomp, 0, srcfmt, rawdata, dstfmt, finaldata); UpdateAviSurface(); backsurf->BltFast( x, y, avisurf, 0, DDBLTFAST_WAIT );

curframe=(curframe<endframe) ? curframe+1 : startframe;

primsurf->Flip( 0, DDFLIP_WAIT ); }


Функция DrawScene() с помощью функции AVIStreamRead() извлекает очередной кадр из AVI-потока, после чего сохраняет полученные данные в буфере rawdata. Я оставил в ней несколько макросов TRACE(), которые пригодились мне при отладке, но надеюсь, что вам они не понадобятся.

Затем мы вызываем функцию ICDecompress() и передаем ей логический номер декомпрессора, ранее полученный от функции LoadAvi(). Аргументами функции ICDecompress() являются два буфера — первый содержит необработанные (сжатые) данные, а второй — восстановленное изображение.

Функция UpdateAviSurface() копирует восстановленный кадр на поверхность AVI. Эта функция рассматривается ниже.

Подготовленная поверхность AVI копируется во вторичный буфер функцией BltFast() интерфейса DirectDrawSurface. После этого переменная curframe увеличивается или сбрасывается в зависимости от ее значения и количества кадров в ролике. Наконец, функция Flip() интерфейса DirectDrawSurface выводит кадр на экран.




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


void BumperWin::DrawScene() { ASSERT(nsprites>0); ASSERT(text);

for (int s1=0;s1<nsprites;s1++) for (int s2=s1+1;s2<nsprites;s2++) if (SpritesCollide( sprite[s1], sprite[s2] )) { sprite[s1]->Hit( sprite[s2] ); sprite[s2]->Hit( sprite[s1] ); }

for (int i=0;i<nsprites;i++) sprite[i]->Update();

ClearSurface( backsurf, 0 ); for (i=0;i<nsprites;i++) { Sprite* s=sprite[i]; BltSurface( backsurf, *s, s->GetX(), s->GetY(), TRUE ); }

BltSurface( backsurf, text, 0, 448, TRUE );

primsurf->Flip( 0, DDFLIP_WAIT ); }


Проверка столкновений осуществляется во вложенном цикле. Для каждой пары спрайтов вызывается функция SpritesCollide(), а при обнаруженном столкновении вызывается функция Hit(), которой в качестве аргументов передаются оба столкнувшихся спрайта. Напомню, что функция Sprite::Hit()

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

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

После того как все столкновения будут обнаружены и обработаны, мы стираем вторичный буфер функцией DirectDrawWin::ClearSurface() и выводим каждый спрайт функцией BltSurface(). Обратите внимание на то, что вторым аргументом BltSurface() является указатель на сам объект Sprite. В данном случае оператор LPDIRECTDRAWSURFACE() преобразует объект Sprite

в указатель на поверхность, соответствующую данному спрайту. Также стоит заметить, что координаты спрайтов определяются функциями GetX() и GetY(). После прорисовки всех спрайтов в левом нижнем углу вторичного буфера выводится поверхность меню. Функция Flip() переключает страницы и отображает кадр на экране.



Функция Flip()


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

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



Функция GetCaps()


Интерфейс DirectDraw позволяет точно узнать, какие возможности поддерживаются как на программном, так и на аппаратном уровне. Функция GetCaps() инициализирует два экземпляра структуры DDCAPS. Первая структура показывает, какие возможности поддерживаются непосредственно видеокартой, а вторая — что доступно посредством программной эмуляции. Функция GetCaps() помогает определить, поддерживаются ли нужные возможности.

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

СОВЕТ

DirectX Viewer

В DirectX SDK входит программа DXVIEW, которая сообщает о возможностях всех компонентов DirectX, в том числе и DirectDraw. На большинстве компьютеров информация о DirectDraw отображается в двух категориях: Primary Display Driver и Hardware Emulation Layer. Первая категория сообщает о возможностях аппаратных видеосредств. Во второй перечислены возможности, эмулируемые DirectDraw при отсутствии аппаратной поддержки. На компьютерах с двумя и более видеокартами, поддерживаемыми DirectDraw, DXVIEW выводит сведения о способностях каждой из них.



Функция GetDDInterface()


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



Функция GetFourCCCodes()


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



Функция InstallPalette()


С помощью файлового формата AVI и VFWAPI можно получить палитру, оптимально подходящую для просмотра видеоролика. Функция InstallPalette() извлекает необходимые данные и использует их для конструирования палитры DirectDraw. Функция InstallPalette() выглядит так:


BOOL AviPlayWin::InstallPalette() { ICDecompressGetPalette( decomp, srcfmt, dstfmt );

PALETTEENTRY pe[256]; LPBITMAPINFO info=(LPBITMAPINFO)dstfmt; for (int i=0; i<256; i++) { pe[i].peRed = info->bmiColors[i].rgbRed; pe[i].peGreen = info->bmiColors[i].rgbGreen; pe[i].peBlue = info->bmiColors[i].rgbBlue; pe[i].peFlags = 0; }

if (avipal) avipal->Release(); ddraw2->CreatePalette( DDPCAPS_8BIT, pe, &avipal, 0 );

primsurf->SetPalette( avipal );

return TRUE; }


Функция ICDecompressGetPalette()

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



Функция LoadAvi()


Перейдем к функции, которая непосредственно открывает AVI-файл. Функция LoadAvi() приведена в листинге8.3.

Листинг 8.3. Функция LoadAvi()


BOOL AviPlayWin::LoadAvi() { long r; CWaitCursor cur;

if (avistream) AVIStreamRelease( avistream ), avistream=0; r=AVIStreamOpenFromFile( &avistream, filename, streamtypeVIDEO, 0, OF_READ | OF_SHARE_EXCLUSIVE, 0 ); TRACE("AVIStreamOpenFromFile: %s\n", r==0 ? "OK" : "failed" );

r=AVIStreamFormatSize( avistream, 0, &fmtlen ); TRACE("AVIStreamFormatSize: %s\n", r==0 ? "OK" : "failed" );

int formatsize=fmtlen+sizeof(RGBQUAD)*256; if (srcfmt) delete [] srcfmt; srcfmt = (LPBITMAPINFOHEADER)new BYTE[formatsize]; ZeroMemory( srcfmt, formatsize );

if (dstfmt) delete [] dstfmt; dstfmt = (LPBITMAPINFOHEADER)new BYTE[formatsize]; ZeroMemory( dstfmt, formatsize );

r=AVIStreamReadFormat( avistream, 0, srcfmt, &fmtlen ); TRACE("AVIStreamReadFormat: %s\n", r==0 ? "OK" : "failed" );

TRACE(" --- %s ---\n", filename); TRACE(" biSize: %d\n", srcfmt->biSize ); TRACE(" biWidth x biHeight: %dx%d\n", srcfmt->biWidth, srcfmt->biHeight ); if (srcfmt->biPlanes != 1) TRACE(" - biPlanes: %d\n", srcfmt->biPlanes ); TRACE(" biBitCount: %d\n", srcfmt->biBitCount );

CString comp; switch (srcfmt->biCompression) { case BI_RGB: comp="BI_RGB"; break; case BI_RLE8: comp="BI_RLE8"; break; case BI_RLE4: comp="BI_RLE4"; break; case BI_BITFIELDS: comp="BI_BITFIELDS"; break; } TRACE(" biCompression: %s\n", comp );

TRACE(" biSizeImage: %d\n", srcfmt->biSizeImage ); TRACE(" ------------------\n");

memcpy( dstfmt, srcfmt, fmtlen ); dstfmt->biBitCount = 8; dtfmt->biCompression = BI_RGB; dstfmt->biSizeImage = dstfmt->biWidth * dstfmt->biHeight;

startframe = AVIStreamStart( avistream ); TRACE("stream start: %d\n", startframe);

endframe = AVIStreamEnd( avistream ); TRACE("stream end: %d\n", endframe );

r=AVIStreamInfo( avistream, &streaminfo, sizeof(streaminfo) ); TRACE("AVIStreamInfo: %s\n", r==0 ? "OK" : "failed" );

buflen = dstfmt->biSizeImage; int finalbuflen=((dstfmt->biWidth+3) & ~3) * dstfmt->biHeight;

if (streaminfo.dwSuggestedBufferSize) if ((LONG)streaminfo.dwSuggestedBufferSize < buflen) { TRACE("adjusting buflen to suggested size\n"); buflen = (LONG)streaminfo.dwSuggestedBufferSize; }

if (decomp) ICClose( decomp ); decomp = ICDecompressOpen( ICTYPE_VIDEO, streaminfo.fccHandler, srcfmt, dstfmt ); TRACE("ICDecompressOpen: %s\n", decomp ? "OK" : "failed");

if (rawdata) { TRACE("delete [] rawdata...\n"); delete [] rawdata; } rawdata = new BYTE[buflen];

if (finaldata) { TRACE("delete [] finaldata...\n"); delete [] finaldata; } finaldata = new BYTE[finalbuflen];

return TRUE; }

<
br>В функции LoadAvi() используются функции VFW. Сначала LoadAvi() закрывает открытый ранее AVI-поток функцией AVIStreamRelease(), а затем открывает новый поток функцией AVIStreamOpenFromFile(), которой в числе прочих аргументов передается имя открываемого AVI-файла.

Обратите внимание — третьим аргументом функции AVIStreamOpenFromFile() является флаг, определяющий тип открываемого потока. В нашем случае использован флаг видеопотока streamtypeVIDEO, но с помощью трех оставшихся флагов (streamtypeAUDIO, streamtypeMIDI и streamtypeTEXT) можно открывать и потоки других типов.

Затем мы получаем данные о формате потока функцией AVIStreamReadFormat() (пользуясь при этом функцией AVIStreamFormatSize()). Я специально оставил в этом фрагменте отладочные макросы TRACE(), чтобы продемонстрировать, какую информацию можно получить об AVI-файле.

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

Затем мы получаем доступ к декомпрессору. Функция ICDecompressorOpen()

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


Функция OnCreate()


Мы будем рассматривать функции примерно в порядке их выполнения. Начнем с функции OnCreate(), которая выглядит так:


int AviPlayWin::OnCreate(LPCREATESTRUCT lpCreateStruct) { if (DirectDrawWin::OnCreate(lpCreateStruct) == -1) return -1;

AVIFileInit();

ShowDialog();

return 0; }


Сначала мы вызываем версию OnCreate()

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

Затем мы вызываем функцию AVIFileInit(), которая инициализирует Video For Windows. После этого можно спокойно пользоваться функциями VFW.

Наконец, функция ShowDialog() выводит диалоговое окно для выбора AVI-файла и ожидает ввод от пользователя. Однако перед тем, как обсуждать ShowDialog(), необходимо рассмотреть функцию SelectInitialDisplayMode(), которая вызывается при использовании функции OnCreate() класса DirectDrawWin.



Функция OnDestroy()


Остается лишь завершить приложение. Функция OnDestroy() занимается «уборкой мусора»— она закрывает открытые AVI-потоки, освобождает декомпрессор и буферы данных AVI:


void AviPlayWin::OnDestroy() { DirectDrawWin::OnDestroy(); if (avistream) AVIStreamRelease( avistream ), avistream=0; if (decomp) ICClose( decomp ), decomp=0; if (srcfmt) delete [] srcfmt, srcfmt=0; if (dstfmt) delete [] dstfmt, dstfmt=0;

if (rawdata) { TRACE("delete [] rawdata...\n"); delete [] rawdata, rawdata=0; } if (finaldata) { TRACE("delete [] finaldata..\n"); delete [] finaldata, finaldata=0;; }

if (avidialog) delete avidialog, avidialog=0;

AVIFileExit(); }


Обратите внимание на вызов функции AviFileExit() в конце OnDestroy(). Это завершает работу VFW и освобождает все используемые им ресурсы.



Функция OnKeyDown()


Как видно из меню, программа Bumper реагирует на две клавиши: пробел и Escape. Нажатие пробела приводит к тому, что векторы направлений каждого спрайта пересчитываются заново, а Escape завершает работу программы. Функция OnKeyDown() выглядит так:


void BumperWin::OnKeyDown(UINT nChar, UINT nRepCnt, UINT nFlags) { switch (nChar) { case VK_ESCAPE: PostMessage( WM_CLOSE ); break; case VK_SPACE: case VK_RETURN: for (int i=0;i<nsprites;i++) sprite[i]->CalcVector(); break; }

DirectDrawWin::OnKeyDown(nChar, nRepCnt, nFlags); }




Функция RestoreSurfaces()


Все трудное осталось позади, дальше будет легко. Особенно просто реализуется функция RestoreSurfaces():


void AviPlayWin::RestoreSurfaces() { avisurf->Restore(); }


Вспомните — функция RestoreSurfaces() вызывается только при восстановлении потерянных поверхностей, а класс DirectDrawWin

автоматически восстанавливает первичную поверхность со вторичным буфером. В программе AviPlay остается лишь восстановить поверхность AVI, а для этого достаточно вызвать функцию Restore() интерфейса DirectDrawSurface.

В некоторых программах функция RestoreSurfaces()

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



Функция SelectInitialDisplayMode()


Как упоминалось выше, функция SelectInitialDisplayMode() решает три задачи. Она выглядит так:


int AviPlayWin::SelectInitialDisplayMode() { GetSystemPalette(); int i, nummodes=GetNumDisplayModes(); DWORD w,h,d; for (i=0;i<nummodes;i++)

{ DisplayModeDescription desc; GetDisplayModeDimensions( i, w, h, d ); if (d==8) { desc.w=w; desc.h=h; desc.d=d; desc.desc.Format( "%dx%d", w, h ); displaymode.Add( desc ); } }

int curdepth=GetDisplayDepth(); if (curdepth!=8) ddraw2->SetDisplayMode( 640, 480, curdepth, 0, 0 ); for (i=0;i<nummodes;i++) { GetDisplayModeDimensions( i, w, h, d ); if (w==640 && h==480 && d==8) return i; }

return 1; }


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

создает палитру DirectDraw на базе текущей палитры Windows. Эта палитра обеспечивает правильный вывод диалогового окна независимо от того, какая палитра была установлена для воспроизведения ролика. Вспомните— GDI ничего не знает о DirectDraw и поэтому всегда пытается вывести диалоговое окно с использованием системной палитры, несмотря на то что она могла быть переопределена DirectDraw.

Затем функция SelectInitialDisplayMode() перебирает список доступных видеорежимов и сохраняет описания 8-битных режимов в массиве displaymodes. Позднее этот массив передается диалоговому окну для вывода списка доступных видеорежимов.

Наконец, функция ищет 8-битный режим с разрешением 640x480. Этот режим выбран лишь потому, что он поддерживается абсолютным большинством видеокарт (если не всеми). После вывода диалогового окна пользователь сможет выбрать любой другой 8-битный режим.



Функция SetCooperativeLevel()


Функция SetCooperativeLevel() определяет уровень кооперации — степень контроля над видеокартой, необходимую для данного приложения. Например, нормальный (normal) уровень кооперации означает, что приложение не сможет изменить текущий видеорежим или задать содержимое всей системной палитры. Монопольный (exclusive) уровень допускает переключение видеорежимов и предоставляет приложению полный контроль над палитрой. Независимо от выбранного уровня вам необходимо вызвать SetCooperativeLevel().



Функция SetDisplayMode()


Существуют две версии функции SetDisplayMode(). Первая из них принадлежит интерфейсу DirectDraw и вызывается с тремя аргументами. Вторая версия принадлежит интерфейсу DirectDraw2 и вызывается с пятью аргументами. Прототип первой функции выглядит так:


HRESULT SetDisplayMode( DWORD width, DWORD height, DWORD depth );

СОВЕТ
Для любознательных читателей

Заглянув в заголовочный файл DirectDraw (ddraw.h), вы не найдете в нем этого прототипа. Это объясняется тем, что все функции DirectDraw описываются на IDL (языке определения интерфейсов) спецификации COM. На IDL функция SetDisplayMode() выглядит так:


STDMETHOD(SetDisplayMode)(THIS_ DWORD, DWORD, DWORD) PURE;

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

Функция SetDisplayMode(), как и большинство функций DirectX API, возвращает значение типа HRESULT - 32-разрядную величину с описанием результата вызова функции. Ее значение DD_OK показывает, что вызов оказался успешным.

Версия SetDisplayMode() из интерфейса DirectDraw получает три аргумента типа DWORD. Эти аргументы определяют разрешение экрана и глубину пикселей нужного видеорежима, поэтому стандартный видеорежим VGA 640x480x8 активизируется так:


ddraw1->SetDisplayMode( 640, 480, 8 );

Выглядит довольно просто, поэтому давайте перейдем к версии SetDisplayMode() из интерфейса DirectDraw2. Ее традиционный прототип выглядит так:


HRESULT SetDisplayMode( DWORD width, DWORD height, DWORD depth, DWORD refreshrate, DWORD flags );


В этой версии появляются два дополнительных аргумента: частота смены кадров и двойное слово, которое может быть использовано в будущих версиях DirectDraw, а пока должно быть равно нулю. В расширенной версии SetDisplayMode() стандартный видеорежим VGA 640x480x8 можно активизировать так:


ddraw1->SetDisplayMode( 640, 480, 8, 0, 0 );


данном случае вместо частоты смены кадров передается 0; это означает, что должна быть использована частота, принятая по умолчанию. Кроме того, можно указать конкретное значение частоты (60 Гц в следующем примере):


ddraw1->SetDisplayMode( 640, 480, 8, 60, 0 );


Однако не следует думать, что вы можете задать любую частоту (или другие параметры видеорежима). Перед тем как вызывать SetDisplayMode(), необходимо сначала определить параметры и частоты допустимых видеорежимов.



Функция ShowDialog()


Давайте рассмотрим функцию для вывода диалогового окна. Функция ShowDialog() приведена в листинге 8.2.

Листинг 8.2. Функция ShowDialog()


void AviPlayWin::ShowDialog() { const CRect& displayrect=GetDisplayRect(); if (displayrect.Width()<640 || displayrect.Height()>480) ddraw2->SetDisplayMode( 640, 480, 8, 0, 0 );

ClearSurface( backsurf, 0 ); ClearSurface( primsurf, 0 ); primsurf->SetPalette( syspal );

ddraw2->FlipToGDISurface(); ShowCursor( TRUE );

if (avidialog==0) { avidialog=new AviDialog(); avidialog->SetArray( &displaymode ); }

if (avistream) AVIStreamRelease( avistream ), avistream=0;

if (avidialog->DoModal()==IDCANCEL) { PostMessage( WM_CLOSE ); return; } ShowCursor( FALSE );

fullfilename=avidialog->fullfilename; filename=avidialog->filename; pathname=avidialog->pathname;

int index=avidialog->GetIndex(); DWORD w,h,d;

w=displaymode[index].w; h=displaymode[index].h; d=displaymode[index].d; ActivateDisplayMode( GetDisplayModeIndex( w, h, d ) );

LoadAvi(); CreateAviSurface(); InstallPalette(); curframe=startframe; }


Функция ShowDialog()

начинается с проверки текущего разрешения. Если в данный момент установлен видеорежим с разрешением меньше 640x480, он изменяется. Это сделано для того, чтобы диалоговое окно не выводилось в режиме Mode X. Поскольку этот режим не поддерживается Windows, такая попытка, скорее всего, закончится неудачей из-за нелинейной организации пикселей в режимах Mode X.

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

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



После установки системной палитры мы вызываем функцию DirectDraw FlipToGDISurface(). Это гарантирует, что диалоговое окно Windows будет отображаться на видимой поверхности, а не во вторичном буфере. Кроме того, мы снова включаем курсор мыши (иначе пользователь не сможет нажимать кнопки диалогового окна и выбрать AVI-файл).

Если экземпляр класса AviDialog не был создан при предыдущем вызове функции ShowDialog(), мы создаем его. Обратите внимание на то, что при создании диалогового окна ему передается массив 8-битных видеорежимов, подготовленный в функции SelectInitialDisplayMode().

Затем мы закрываем существующий AVI-поток. Это делается из-за того, что класс AviDialog

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

Функция DoModal() отображает диалоговое окно, в котором пользователь может выбрать нужный файл. При нажатии кнопки Cancel мы посылаем сообщение WM_CLOSE. Если все идет нормально, мы получаем имя выбранного файла (в трех различных формах) вместе с индексом видеорежима (видеорежим необходимо выбрать до нажатия кнопки Play). Размеры выбранного видеорежима, взятые из массива displaymode, передаются функции SetDisplayMode().

Дальше следует вызов функции LoadAvi(). Как вы вскоре убедитесь, функция LoadAvi()

на самом деле не загружает видеоролик — она лишь открывает файл и извлекает сведения о ролике (например, количество кадров и их размеры). Функция CreateAviSurface() по полученным размерам создает поверхность для хранения одного кадра видеопотока.

Функция InstallPalette()

извлекает данные палитры из AVI-файла и строит по ним палитру DirectDraw, которая лучше всего подходит для просмотра. Наконец, переменной curframe, предназначенной для перебора кадров, присваивается значение переменной startframe.


Функция UpdateAviSurface()


Перед тем как рассматривать функцию UpdateAviSurface(), я хочу обратить ваше внимание на ее сходство с кодом класса DirectDrawWin, предназначенным для загрузки BMP-файлов на поверхность (см. главу5). Функция UpdateAviSurface(), как и функции загрузки BMP-файлов DirectDrawWin, блокирует поверхность и затем копирует данные в ее память:


BOOL AviPlayWin::UpdateAviSurface() { HRESULT r; if (finaldata==0) return FALSE; DWORD dwWidth = (srcfmt->biWidth+3) & ~3; DWORD dwHeight = srcfmt->biHeight; DDSURFACEDESC desc; ZeroMemory( &desc, sizeof(desc) ); desc.dwSize = sizeof(desc);

r = avisurf->Lock( 0, &desc, DDLOCK_WAIT, 0 ); if (r==DD_OK) { BYTE* src = finaldata + dwWidth * (dwHeight-1); BYTE* dst = (BYTE *)desc.lpSurface;

for (DWORD y=0; y<dwHeight; y++) { memcpy( dst, src, dwWidth ); dst += desc.lPitch; src -= dwWidth; } avisurf->Unlock( 0 ); }

return TRUE; }


После блокировки поверхности функция UpdateAviSurface()

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



Где достать DirectX SDK


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

Перед выходом DirectX я также предупреждал их, что файл SDK занимает 28 Мбайт.

Размер DirectX 3 SDK разочаровал многих читателей, а некоторые из-за этого даже не смогли работать с ним. 28 Мбайт — громадный объем для всех, у кого нет доступа к Internet через ISDN или T1, и при этом он мог бы быть и поменьше. Большую часть этих 28 Мбайт занимают примеры программ, без которых разработчик может обойтись. Строго говоря, действительно необходимы лишь H- и LIB-файлы, которые сжимаются до 100 Кб. Справочный файл сокращается до 4 Мбайт. Разумеется, эти компоненты можно было бы сделать доступными и по отдельности.

В августе 1997 года (на момент написания этой книги) Microsoft выпустила DirectX 5 и выложила его на свою Web-страницу. На этот раз ей значительно лучше удалось удержать размер файлов в разумных пределах. Эти файлы можно найти по адресу www.microsoft.com/directx.



С. Трухильо. Графика для Windows, б-ка программистаwww.piter.comprog.dax.ru



Краткий курс DirectDraw


Что такое DirectDraw?

Спецификация COM фирмы Microsoft

DirectDraw API

Подготовка к работе

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

И все же я решил попробовать.

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

Кроме того, я пропускаю многословные рассуждения о HAL (Hardware Abstraction Layer, прослойка абстрактной аппаратуры), HEL (Hardware Emulation Layer, прослойка эмуляции аппаратуры) и все кошмарные диаграммы, которые встречаются в справочных файлах SDK и некоторых книгах по DirectDraw. Вы читаете эту книгу, чтобы освоить программирование для DirectDraw, а не потому, что собираетесь писать драйверы устройств DirectDraw или изучать тонкости внутреннего устройства библиотеки.

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



Проблемы быстродействия


Оптимизация

C и C++

Быстродействие DirectDraw

Будущее DirectX

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

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

Не стоит полагать, что каждый пользователь, знакомясь с новой программой, говорит себе: «Пусть эта штуковина работает побыстрее, а не то…» Быстродействие программы чаще оценивается на подсознательном уровне. Работа с быстрой программой, мгновенно реагирующей на все ваши желания, доставляет радость. Хорошее быстродействие вселяет в пользователя уверенность и желание работать дальше. Медленные программы лишь испытывают наше терпение. Каждый, кому приходится работать с ними, мечтают поскорее закончить свои мучения. Пользователи предпочитают, чтобы любая программа (текстовый или графический редактор, игра или любое другое приложение) быстро и адекватно реагировала на их действия.

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



За кулисами DirectDraw


DirectDraw AppWizard

Структура приложения

Инициализация DirectDraw

Создание поверхностей

Графический вывод

Пора заняться программированием. В этой главе мы создадим несложное приложение DirectDraw и обсудим практически каждую строку его кода. Для создания демонстрационной программы Bounce будет применен мастер DirectDraw AppWizard с прилагаемого CD-ROM. Затем мы изучим структуру классов и подробности реализации этой программы. К моменту завершения у вас появится четкое понимание того, как работают приложения DirectDraw (и, возможно, легкая головная боль).



Видеорежимы и частота смены кадров


Переключение видеорежимов

Частота смены кадров

Использование цветовых ключей

Измерение FPS (количества кадров в секунду)

Вывод текста на поверхностях DirectDraw

В главе 1 я упоминал функции EnumDisplayModes() и SetDisplayMode() интерфейса DirectDraw и говорил о том, что они применяются для обнаружения и активизации видеорежимов. Здесь эти функции будут применены на практике. Сначала мы познакомимся с общими принципами переключения видеорежимов, а затем рассмотрим две демонстрационных программы: Switch и SuperSwitch. Программа Switch выводит меню с обнаруженными видеорежимами и позволяет последовательно активизировать каждый из них. Программа SuperSwitch в дополнение к этому позволяет выбрать частоту смены кадров для каждого видеорежима.

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

Кроме того, эта глава преследует и другую цель. В программах Switch и SuperSwitch используются некоторые средства DirectDraw, которые, скорее всего, вам пригодятся (или уже пригодились), — например, цветовые ключи и вывод текста на поверхности. Хотя у нас нет времени для подробного изучения этих тем, мы кратко рассмотрим их в этой главе, чтобы в дальнейшем никакие объяснения уже не понадобились.



Поверхности и форматы пикселей

Палитровые поверхности

Поверхности High Color

Поверхности True Color

Загрузка графических данных на поверхность

Использование диалоговых окон Windows в DirectDraw

Наверное, это самая важная глава во всей книге. Она посвящена поверхностям, а поверхности — главное, для чего создавалась библиотека DirectDraw. Поверхности DirectDraw позволяют хранить изображения, копировать и изменять их, переключать кадры и выводить графическую информацию на экран. Все интерфейсы DirectDraw в первую очередь ориентированы на работу с поверхностями. Интерфейс DirectDrawPalette облегчает интерпретацию палитровых поверхностей; интерфейс DirectDrawClipper определяет, какая часть (или части) поверхности будут копироваться при блиттинге; наконец, сам интерфейс DirectDraw обеспечивает основные средства для работы с поверхностями.



DirectInput


Что такое DirectInput?

DirectInput API

Инициализация DirectInput

Ввод с клавиатуры

Ввод от мыши

Давайте отдохнем от DirectDraw и познакомимся с библиотекой DirectInput. Конечно, эта книга посвящена компьютерной графике, но ввод информации пользователем — необходимая часть любого графического приложения. Применение DirectInput улучшает работу программ, так как ввод обнаруживается и обрабатывается с максимальной скоростью. После краткого знакомства с DirectInput API мы обсудим различные схемы получения пользовательского ввода, поддерживаемые DirectInput. Знакомство закончится созданием двух приложений: Qwerty и Smear. Программа Qwerty использует DirectInput для ввода с клавиатуры, а Smear работает с мышью.

Для компиляции и запуска программ этой главы вам понадобится DirectX 3 SDK. Пользователи Windows NT 4.0 должны установить Service Pack 3 или более позднюю версию.



Проблема курсора


В чем суть проблемы?

Частичное обновление экрана

Многопоточность

Решение проблемы курсора

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

Первая трудность связана с переключением страниц. Если курсор мыши не был отключен, Windows не подозревает о том, что видеоустройства находятся под управлением DirectDraw, и выводит курсор мыши на поверхности GDI. Если поверхность GDI скрыта, а вместо нее на экране отображается другая поверхность, курсор исчезает. Эта переменчивость приводит к тому, что курсор мыши мерцает и выглядит полупрозрачным.

Более того, курсор может оказаться искаженным или вообще отсутствовать. Например, в видеорежимах ModeX используется нелинейная организация пикселей. Windows пытается вывести курсор мыши, но поскольку режимы Mode X не поддерживаются Windows GDI, изображение курсора портится. Кроме того, Windows не умеет выводить курсор мыши на вторичных видеоустройствах (например, на видеокартах с чипами 3Dfx). Вывод продолжает поступать на первичное видеоустройство независимо от того, какое устройство активно в данный момент, поэтому курсор мыши пропадает.

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

Раз курсор мыши отображается самим приложением, а не Windows, частота его прорисовки зависит от быстродействия приложения. Если приложение постоянно работает на 75 FPS, это будет приемлемо, но что если частота вывода кадров упадет до 30 FPS и ниже? С падением частоты курсор будет все медленнее реагировать на действия пользователя.



Существует множество причин, по которым приложение может иметь низкую частоту вывода кадров. Трехмерные приложения предъявляют особенно жесткие требования к системе и часто работают с частотой 30 FPS и менее. Но «тормозить» могут и обычные, не трехмерные приложения. Сложное приложение, выводящее сотни объектов, будет иметь низкий FPS (конечно, все зависит от компьютера и видеокарты). Кроме того, режимы High и True Color часто оказываются намного медленнее 8-битных режимов.

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

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

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

Воспроизведение видеороликов


Основные принципы работы с видео

AVI-файлы

Video For Windows API

Программа AviPlay

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

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



Проверка столкновений


Традиционные методы

Как обеспечить точность на уровне пикселей

Класс Sprite

Программа Bumper

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

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

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

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



Глубина пикселей


Глубина пикселей показывает, сколько разных цветов может быть представлено одним пикселем поверхности. Глубина пикселей также влияет на объем памяти, необходимой для представления поверхности. В DirectDraw предусмотрена поддержка четырех глубин пикселей: 8-битных (палитровых), 16-битных (High Color), 24-битных и 32-битных (объединяемых термином True Color).

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

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

Пиксели High Color (16-битные) выглядят несколько сложнее, однако результат часто оправдывает усилия. Простота использования, характерная для беспалитровых поверхностей, сочетается в них с умеренным расходом памяти (по сравнению с пикселями глубины True Color). Каждый пиксель High Color содержит не индекс, а цвет. Цвета выражаются в виде комбинации трех цветовых составляющих: красной, зеленой и синей (RGB).

Пиксели True Color делятся на две категории (24- и 32-битные), но в обоих случаях используются только 24 бита данных RGB. Лишние 8 бит 32-битных пикселей иногда используются для хранения альфа-канала (то есть данных о прозрачности пикселя). К сожалению, в DirectDraw все еще отсутствует возможность эмуляции альфа-наложения, так что лишние биты 32-битного пикселя часто пропадают впустую.

Разумеется, достоинства поверхностей True Color отчасти снижаются увеличенным расходом памяти. Сказанное поясняет рис. 5.1, на котором наглядно изображены глубины всех четырех вариантов пикселей.
Хотя рис. 5.1 не содержит никаких особых откровений, он позволяет понять, как представление пикселя в памяти зависит от его глубины. Кроме того, его общая структура будет использоваться в других рисунках данного раздела.


Рис. 5.1. Зависимость требований к памяти от глубины пикселей



Графические данные


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

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

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



Графический вывод


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

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


BOOL DirectDrawApp::OnIdle(LONG) { if (ddwin->PreDrawScene()) ddwin->DrawScene(); return TRUE; }


Функция OnIdle() вызывает функцию DirectDrawWin::PreDrawScene() и в зависимости от полученного результата вызывает функцию DrawScene(). Функция OnIdle() всегда возвращает TRUE, потому что при возврате FALSE MFC перестает ее вызывать. Функция PreDrawScene() реализована так:


BOOL DirectDrawWin::PreDrawScene() { if (window_active && primsurf->IsLost()) { HRESULT r; r=primsurf->Restore(); if (r!=DD_OK) TRACE("can't restore primsurf\n"); r=backsurf->Restore(); if (r!=DD_OK) TRACE("can't restore backsurf\n");

RestoreSurfaces(); }

return window_active; }


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

Потеря поверхностей происходит из-за того, что DirectDraw выделяет занятую видеопамять для других целей. Потерянную поверхность можно легко восстановить, но лишь после того, как приложение станет активным, поэтому перед тем, как восстанавливать поверхности, функция PreDrawScene() ждет установки флага window_active (состояние флага window_active зависит от сообщений WM_ACTIVATEAPP, обрабатываемых функцией DirectDrawWin::OnActivateApp). После восстановления первичной поверхности и вторичного буфера вызывается функция RestoreSurfaces(). Она является чисто виртуальной функцией, которая должна быть реализована в производных классах. Сейчас мы рассмотрим ее возможную реализацию.

Так как функция OnIdle() вызывает DrawScene() лишь после проверки результата PreDrawScene(), DrawScene() будет вызвана лишь в том случае, если приложение активно, а первичная и вторичная поверхности не были потеряны.


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


void BounceWin::DrawScene() { ClearSurface( backsurf, 0 ); CRect client=GetClientRect(); int width=client.Width(); int height=client.Height(); x+=xinc; y+=yinc; if (x<-160 || x>width-160) { xinc=-xinc; x+=xinc; } if (y<-100 || y>height-100) { yinc=-yinc; y+=yinc; }

BltSurface( backsurf, surf1, x, y );

int offsetx=client.left; int offsety=client.top;

RECT srect; srect.left=0; srect.top=0; srect.right=client.Width(); srect.bottom=client.Height();

RECT drect; drect.left=offsetx; drect.top=offsety; drect.right=offsetx+client.Width(); drect.bottom=offsety+client.Height();

primsurf->Blt( &drect, backsurf, &srect, DDBLT_WAIT, 0 ); }


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

Вторая блит-операция копирует содержимое вторичного буфера на первичную поверхность. На этот раз используется функция Blt(), поскольку к первичной поверхности присоединен объект отсечения. Структуры srect и drect типа RECT определяют области источника и приемника, участвующие в блиттинге. Заметьте, что при вычислении области приемника используются переменные offsetx и offsety, в которых хранятся координаты клиентской области окна. Если убрать эти смещения из структуры drect, программа всегда будет выводить изображение в левом верхнем углу экрана независимо от расположения окна.




Графическим выводом в программе Switch занимается функция SwitchWin::DrawScene(). Она отвечает за подготовку кадра во вторичном буфере и переключение страниц, благодаря которому новый кадр отображается на экране. Код функции DrawScene() содержится в листинге 4.4.

Листинг 4.4. Функция SwitchWin::DrawScene()


void SwitchWin::DrawScene() { ClearSurface( backsurf, 0 );

BltSurface( backsurf, bmpsurf, x, y );

x+=xinc; y+=yinc;

const CRect& displayrect=GetDisplayRect(); if (x<-160 || x>displayrect.right-160) { xinc=-xinc; x+=xinc; } if (y<-100 || y>displayrect.bottom-100) { yinc=-yinc; y+=yinc; }

backsurf->BltFast( 0, 0, menusurf, 0, DDBLTFAST_SRCCOLORKEY | DDBLTFAST_WAIT );

UpdateFPSSurface(); if (displayfps) { int x=displayrect.right-fpsrect.right-1; int y=displayrect.bottom-fpsrect.bottom-1; backsurf->BltFast( x, y, fpssurf, 0, DDBLTFAST_SRCCOLORKEY | DDBLTFAST_WAIT ); }

primsurf->Flip( 0, DDFLIP_WAIT ); }


Черный цвет не гарантирован

По умолчанию DirectDraw резервирует два элемента палитры: для черного (индекс 0) и для белого (индекс 255). Поэтому обычно заполнение поверхности нулями равносильно ее заливке черным цветом. Тем не менее в палитрах, созданных с флагом DDSCAPS_ALLOW256, можно задавать все 256 элементов.

Функция DirectDrawWin::CreateSurface() при создании и установке палитры (когда необязательный аргумент use_palette равен TRUE) использует флаг DDSCAPS_ALLOW256, поэтому первый элемент в палитрах наших приложений может быть отличен от черного цвета. Флаг можно удалить, но это нарушит цветопередачу при отображении BMP-файлов, у которых первый и последний элементы палитры отличны от черного и белого цветов соответственно.

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

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


Давайте посмотрим, как в программе SuperSwitch реализована функция DrawScene(). Она похожа на одноименную функцию из программы Switch, за исключением того, что при выборе видеорежима новая версия должна отображать поверхность со списком частот. Функция DrawScene() выглядит так:


void SuperSwitchWin::DrawScene() { ClearSurface( backsurf, 0 );

BltSurface( backsurf, bmpsurf, x, y );

x+=xinc; y+=yinc;

const CRect& displayrect=GetDisplayRect(); if (x<-160 || x>displayrect.right-160) { xinc=-xinc; x+=xinc; } if (y<-100 || y>displayrect.bottom-100) { yinc=-yinc; y+=yinc; }

backsurf->BltFast( 0, 0, modemenusurf, 0, DDBLTFAST_SRCCOLORKEY | DDBLTFAST_WAIT );

if (ratemenu_up) { DWORD w,h; GetSurfaceDimensions( ratemenusurf, w, h ); backsurf->BltFast( (320-w)/2, (200-h)/2, ratemenusurf, 0, DDBLTFAST_WAIT ); }

UpdateFPSSurface(); if (displayfps) { int x=displayrect.right-fpsrect.right; int y=displayrect.bottom-fpsrect.bottom; backsurf->BltFast( x, y, fpssurf, &fpsrect, DDBLTFAST_SRCCOLORKEY | DDBLTFAST_WAIT ); }

primsurf->Flip( 0, DDFLIP_WAIT ); }


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




Функция DrawScene() обновляет экран в зависимости от состояния логической переменной update_screen. Если переменная update_screen равна FALSE, предполагается, что содержимое первичной поверхности не устарело, и делать ничего не нужно. Функция DrawScene() выглядит так:


void BmpViewWin::DrawScene(){ if (update_screen && bmpsurf) { ClearSurface( backsurf, 0 ); BltSurface( backsurf, bmpsurf, x, y ); primsurf->Flip( 0, DDFLIP_WAIT );

update_screen=FALSE; } }


Поскольку текущее положение поверхности рассчитывается в другом месте программы, а функция BltSurface() при необходимости автоматически выполняет отсечение, функция DrawScene() реализуется просто. Если переменная update_screen равна TRUE и существует поверхность для вывода, экран обновляется. Если поверхность не заполняет экран целиком, содержимое вторичного буфера стирается; если заполняет, то в стирании буфера нет необходимости. Затем функция BltSurface() копирует поверхность на вторичный буфер, а функция Flip() отображает изменения на экране. После того как обновление будет завершено, переменной update_screen присваивается значение FALSE.



Графика для Windows средствами DirectDraw


Чтобы обратиться к объекту с запросом о поддержке некоторого интерфейса, используя функцию QueryInterface(), необходимо как-то идентифицировать этот интерфейс. Для этого используется значение GUID (глобально-уникального идентификатора, Globally Unique IDentifier) данного интерфейса. GUID представляет собой 128-битное значение, уникальное для всех практических целей. Значения GUID всех интерфейсов DirectDraw включены в заголовочные файлы DirectX.

Такого краткого введения в COM вполне достаточно для эффективной работы с DirectDraw API. Далее, по мере обсуждения DirectDraw API, вы поймете, насколько важна эта информация.



Инициализация


Перед тем как начинать работу, необходимо создать экземпляры всех основных классов приложения. В нашем случае это будут классы BounceWin и BounceApp. Объект приложения создается способом, традиционным для MFC, то есть объявлением глобального экземпляра:


BounceApp theapp;


Класс BounceApp наследует свои функциональные возможности от DirectDrawApp, и больше ему почти ничего не требуется. Есть всего одно исключение: класс BounceApp отвечает за создание объекта BounceWin. Это происходит в функции InitInstance(), вызываемой MFC при запуске приложения. Функция InitInstance() выглядит так:


BOOL BounceApp::InitInstance() { BounceWin* win = new BounceWin; if (!win->Create( "High Performance Bounce Demo", IDI_ICON )) { AfxMessageBox("Failed to create window"); return FALSE; } m_pMainWnd = win; return DirectDrawApp::InitInstance(); }


Функция InitInstance() создает экземпляр класса BounceWin и вызывает функцию BounceWin::Create(). При вызове Create() необходимо передать два аргумента: строку с названием окна и идентификатор ресурса значка. Хотя название окна не отображается во время работы приложения (потому что приложение занимает весь экран и не имеет строки заголовка), оно будет выводиться в списке задач, а также на панели задач при сворачивании приложения. Если вызов Create() закончится неудачей, то функция InitInstance() возвращает FALSE. По этому признаку MFC узнает о том, что приложение следует аварийно завершить.

Затем переменная m_pMainWnd инициализируется указателем на созданный объект окна. Эта переменная принадлежит классу CWinApp; инициализируя ее, вы сообщаете классу CWinApp о том, каким объектом окна он будет управлять. Если m_pMainWnd не будет присвоен указатель на окно, MFC завершает приложение с ошибкой.

Наконец, мы вызываем функцию DirectDrawApp:InitInstance() и используем полученное от нее значение в качестве результата функции BounceApp::InitInstance(). Функция InitInstance() класса DirectDrawApp выглядит так:


BOOL DirectDrawApp::InitInstance() { ASSERT(m_pMainWnd); m_pMainWnd->ShowWindow(SW_SHOWNORMAL); m_pMainWnd->UpdateWindow(); ShowCursor(FALSE); return TRUE; }

<
br>

Я уже упоминал о том, что MFC требует задать значение переменной m_pMainWnd, но поскольку значение m_pMainWnd используется в этой функции, проверку можно выполнить и самостоятельно. Макрос MFC ASSERT() проверяет значение переменной m_pMainWnd. Если указатель равен нулю, приложение завершается с ошибкой. Если он отличен от нуля, мы вызываем две функции созданного окна: ShowWindow() и UpdateWindow(). Эти функции отображают окно на экране. Наконец, функция ShowCursor() отключает курсор мыши.

Создание и отображение окна завершает процесс инициализации классов DirectDrawApp и BounceApp. Теперь давайте посмотрим, как этот процесс отражается на классах DirectDrawWin и BounceWin.

Как мы уже знаем, функция Create() вызывается из функции BounceApp:: InitInstance(). Она не реализуется классом BounceWin, а наследуется от DirectDrawWin. Функция Create() выглядит так:

BOOL DirectDrawWin::Create(const CString& title,int icon) { CString sClassName; sClassName = AfxRegisterWndClass(CS_HREDRAW | CS_VREDRAW, LoadCursor(0, IDC_ARROW), (HBRUSH)(COLOR_WINDOW + 1), LoadIcon(AfxGetInstanceHandle(), MAKEINTRESOURCE(icon))); return CWnd::CreateEx(WS_EX_TOPMOST, sClassName, title, WS_POPUP, 0,0, 100, 100, 0, 0); }

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

Обратите внимание на то, что создаваемое окно имеет размеры 100x100 (седьмой и восьмой аргументы CreateEx()). Такой размер выбран произвольно. DirectDraw при подключении окна автоматически изменяет его размер так, чтобы оно занимало весь экран. Также обратите внимание на флаг WS_EX_TOPMOST: окно полноэкранного приложения DirectDraw должно выводиться поверх остальных окон.

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



Второй аргумент представляет собой адрес указателя на интерфейс DirectDrawClipper. В нашем случае используется переменная класса DirectDrawWin с именем clipper.

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

Далее объект отсечения присоединяется к первичной поверхности приложения функцией SetClipper() интерфейса DirectDrawSurface. После такого присоединения можно осуществлять блиттинг на первичную поверхность с помощью функции Blt() интерфейса DirectDrawSurface. Использовать функцию BltFast() нельзя, потому что она не поддерживает отсечения.

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

Функция CreateFlippingSurfaces() пытается создать «идеальный» вторичный буфер, для чего используются флаг DDSCAPS_VIDEOMEMORY и функция CreateSurface(). Если вызов заканчивается успешно, флаг videobacksurf получает значение TRUE, а функция завершает работу. В противном случае вторичный буфер не создается, а флагу videobacksurf присваивается значение FALSE.

В том варианте вторичный буфер создается приложением в системной памяти позднее, в обработчике OnSize().


Функция OnSize() вызывается при изменении размеров окна приложения. Создавая вторичный буфер по размерам клиентской области окна, мы экономим память. Функция OnSize() выглядит так:

void DirectDrawWin::OnSize(UINT nType, int cx, int cy) { CWnd::OnSize(nType, cx, cy);

CFrameWnd::GetClientRect( &clientrect ); CFrameWnd::ClientToScreen( &clientrect );

if (videobacksurf) return;

DDSURFACEDESC desc; ZeroMemory( &desc, sizeof(desc) ); desc.dwSize = sizeof(desc); desc.dwFlags = DDSD_WIDTH | DDSD_HEIGHT | DDSD_CAPS; desc.dwWidth = clientrect.Width(); desc.dwHeight = clientrect.Height(); desc.ddsCaps.dwCaps = DDSCAPS_OFFSCREENPLAIN | DDSCAPS_SYSTEMMEMORY; if (backsurf) backsurf->Release(), backsurf=0;

HRESULT r=ddraw2->CreateSurface( &desc, &backsurf, 0 ); if (r!=DD_OK) { TRACE("failed to create 'backsurf'\n"); return; } else TRACE("backsurf w=%d h=%d\n", clientrect.Width(), clientrect.Height() );

}

Инициализация приложения завершается вызовом функций StorePixelFormatData() и CreateCustomSurfaces(), происходящим в обработчике OnCreate(). Обе функции ведут себя точно так же, как и в полноэкранном приложении.




Инициализация DirectDraw


Фактическое создание окна (вызов функции CreateEx()) заставляет Windows послать нашему приложению сообщение WM_CREATE. Класс DirectDrawWin перехватывает это сообщение в обработчике OnCreate(), созданном ClassWizard (см. листинг 3.1).

Листинг 3.1. Функция DirectDrawWin::OnCreate()


int DirectDrawWin::OnCreate(LPCREATESTRUCT) { pDirectDrawEnumerate(DriverAvailable, this); if (totaldrivers == 0) { AfxMessageBox("No DirectDraw drivers detected"); return -1; }

int driverindex=SelectDriver(); if (driverindex < 0) { TRACE("No DirectDraw driver selected\n"); return -1; } else if (driverindex > totaldrivers-1) { pAfxMessageBox("Invalid DirectDraw driver selected\n"); return -1; }

LPDIRECTDRAW ddraw1; DirectDrawCreate(driver[driverindex].guid, &ddraw1, 0); HRESULT r; r=ddraw1->QueryInterface(IID_IDirectDraw2, (void**)&ddraw2); if (r!=S_OK) { AfxMessageBox("DirectDraw2 interface not supported"); return -1; } ddraw1->Release(), ddraw1=0;

ddraw2->SetCooperativeLevel(GetSafeHwnd(), DDSCL_EXCLUSIVE|DDSCL_FULLSCREEN|DDSCL_ALLOWMODEX);

ddraw2->EnumDisplayModes(0, 0, this, DisplayModeAvailable); qsort(displaymode, totaldisplaymodes, sizeof(DisplayModeInfo), CompareModes); int initmode=SelectInitialDisplayMode(); if (ActivateDisplayMode(initmode) == FALSE) return -1;

return 0; }


Вся инициализация DirectDraw выполняется в функции OnCreate() (при поддержке нескольких вспомогательных функций). Процесс инициализации состоит из семи этапов:

Получение списка всех драйверов DirectDraw.

Выбор драйвера DirectDraw.

Инициализация DirectDraw с использованием выбранного драйвера.

Получение списка поддерживаемых видеорежимов.

Выбор исходного видеорежима.

Активизация выбранного видеорежима.

Создание поверхностей приложения.

Все эти этапы рассматриваются в последующих разделах.


Третья задача, выполняемая функцией OnCreate(), — инициализация DirectDraw. Я снова привожу соответствующий фрагмент листинга 3.1:


LPDIRECTDRAW ddraw1; DirectDrawCreate( driver[driverindex].guid, &ddraw1, 0 ); HRESULT r; r=ddraw1->QueryInterface( IID_IDirectDraw2, (void**)&ddraw2 ); if (r!=S_OK) { AfxMessageBox("DirectDraw2 interface not supported"); return -1; } ddraw1->Release(), ddraw1=0;

ddraw2->SetCooperativeLevel( GetSafeHwnd(), DDSCL_EXCLUSIVE | DDSCL_FULLSCREEN | DDSCL_ALLOWMODEX );


Сначала мы объявляем ddraw1, указатель на интерфейс DirectDraw. Это локальный и, следовательно, временный указатель. Класс DirectDrawWin объявляет ddraw2, указатель на интерфейс DirectDraw2, однако мы не сможем инициализировать его без интерфейса DirectDraw. Функция DirectDrawCreate() инициализирует указатель ddraw1. Первый аргумент является указателем на GUID выбранного драйвера. Адрес указателя ddraw1 передается в качестве второго аргумента. Последний аргумент DirectDrawCreate() должен быть равен 0.

Рис. 3.9. Диалоговое окно для выбора драйвера

После того как интерфейс DirectDraw будет инициализирован, им можно воспользоваться для получения указателя на интерфейс DirectDraw2. Для этого следует вызвать функцию QueryInterface() и передать ей GUID интерфейса DirectDraw2, определенный под именем IID_IDirectDraw2. Если вызов QueryInterface() заканчивается неудачно, программа выводит диалоговое окно и завершает работу. Фактически мы требуем присутствия библиотеки DirectX версии 2 и выше (потому что интерфейс DirectDraw2 впервые появился в DirectX 2). Если вызов QueryInterface() окажется успешным, указатель ddraw1 освобождается. Попеременный вызов функций интерфейсов DirectDraw и DirectDraw2 не рекомендуется, поэтому освобождение указателя на интерфейс DirectDraw гарантирует, что в оставшейся части кода будет использоваться только интерфейс DirectDraw2.

Затем мы вызываем функцию SetCooperativeLevel() и в качестве аргументов передаем ей логический номер нашего окна и три флага. По логическому номеру организуется взаимодействие окна с DirectDraw. При вызове SetCooperativeLevel() использованы три флага: DDSCL_EXCLUSIVE, DDSCL_FULLSCREEN и DDSCL_ALLOWMODEX. Флаги монопольного и полноэкранного режима обычно используются вместе для получения максимальных полномочий по управлению видеоустройством. Последний флаг означает, что все поддерживаемые видеорежимы Mode X должны быть доступны для выбора в программе. В Windows NT этот флаг игнорируется.



Инициализация DirectInput


Инициализация DirectInput и DirectDraw выполняется в функции OnCreate(). DirectInput инициализируется версией OnCreate() класса QwertyWin, а DirectDraw — версией из DirectDrawWin. Функция QwertyWin::OnCreate() приведена в листинге 6.2.

Листинг 6.2. Функция QwertyWin::OnCreate()


int QwertyWin::OnCreate(LPCREATESTRUCT lpCreateStruct) { HRESULT r=DirectInputCreate( AfxGetInstanceHandle(), DIRECTINPUT_VERSION, &dinput, 0 ); if (r!=DI_OK) { AfxMessageBox("DirectInputCreate() failed"); return -1; }

r = dinput->CreateDevice( GUID_SysKeyboard, &keyboard, 0 ); if (r!=DI_OK) { AfxMessageBox("CreateDevice(keyboard) failed"); return -1; }

r = keyboard->SetDataFormat( &c_dfDIKeyboard ); if (r!=DI_OK) { AfxMessageBox("keyboard->SetDataFormat() failed"); return -1; } r=keyboard->SetCooperativeLevel( GetSafeHwnd(), DISCL_FOREGROUND | DISCL_NONEXCLUSIVE); if (r!=DI_OK) { AfxMessageBox("keyboard->SetCooperativeLevel() failed"); return -1; } if (DirectDrawWin::OnCreate(lpCreateStruct)==-1) return -1;

return 0; }


Прежде всего обратите внимание — версия OnCreate() базового класса вызывается лишь в конце функции. Это сделано для того, чтобы при неудачной инициализации DirectInput программа выводила окно сообщения и прекращала работу без инициализации DirectDraw.

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


HRESULT r=DirectInputCreate( AfxGetInstanceHandle(), DIRECTINPUT_VERSION, &dinput, 0 );


Первый аргумент - логический номер экземпляра приложения, получаемый функцией AfxGetInstanceHandle(). Второй аргумент — номер версии DirectInput. В нашем случае используется константа DIRECTINPUT_VERSION, она определяется DirectInput в зависимости от версии SDK, использованной для компиляции приложения. Различные версии DirectInput более подробно рассматриваются в этой главе ниже.
Третий аргумент DirectInputCreate() — адрес инициализируемого указателя, а четвертый — показатель агрегирования COM, который обычно равен нулю (агрегированием называется разновидность наследования, используемая в COM). Если инициализация DirectInput проходит успешно (то есть если DirectInputCreate() возвращает DI_OK), указатель dinput может использоваться для работы с DirectInput.

Затем мы создаем экземпляр интерфейса DirectInputDevice, который представляет клавиатуру. Я снова приведу соответствующую строку
листинга 6.2:

r = dinput->CreateDevice( GUID_SysKeyboard, &keyboard, 0 );


Функция CreateDevice() интерфейса DirectInput применяется для инициализации устройств DirectInput. В нашем случае первым аргументом является стандартная константа GUID_SysKeyboard, показывающая, что мы собираемся работать с системной клавиатурой. Второй аргумент — адрес указателя keyboard, через который мы впоследствии будем обращаться к клавиатуре. Третий аргумент — показатель агрегирования COM, в нашем случае он должен быть равен нулю.

Следующий шаг — выбор формата данных устройства. Для клавиатуры он выполняется просто:

r = keyboard->SetDataFormat( &c_dfDIKeyboard );


Функции SetDataFormat() интерфейса DirectInputDevice передается единственный аргумент — константа стандартного формата c_dfDIKeyboard. Программа Qwerty работает лишь с одним устройством (клавиатурой), но, как мы убедимся в программе Smear, формат данных должен задаваться отдельно для каждого устройства, используемого программой.

Затем мы задаем уровень кооперации устройства с помощью функции SetCooperativeLevel() интерфейса DirectInputDevice. Соответствующий фрагмент листинга 6.2 выглядит так:

r=keyboard->SetCooperativeLevel( GetSafeHwnd(), DISCL_FOREGROUND | DISCL_NONEXCLUSIVE);


Функция SetCooperativeLevel() получает два аргумента: логический номер окна и набор флагов, определяющих уровень кооперации. Функция GetSafeHwnd() определяет логический номер окна, а флаги DISCL_FOREGROUND и DISCL_NONEXCLUSIVE задают нужный уровень кооперации.Флаг активного режима DISCL_FOREGROUND присутствует потому, что на время активности другого приложения нам не потребуется ввод от клавиатуры, а флаг DISCL_NONEXCLUSIVE — потому, что DirectInput не позволяет установить монопольный доступ к клавиатуре.

До получения данных с клавиатуры остался всего один шаг: мы должны захватить устройство функцией Acquire(). Эта задача решается функцией OnActivate(), которую мы рассмотрим ниже.

Функция QwertyWin::OnCreate() завершается вызовом функции DirectDrawWin::OnCreate(), инициализирующей DirectDraw. Эта функция обсуждалась в главе 3.


Инициализация клавиатуры


Инициализация клавиатуры выполняется функцией InitKeyboard():


BOOL SmearWin::InitKeyboard() { HRESULT r; r = dinput->CreateDevice( GUID_SysKeyboard, &keyboard, 0 ); if (r!=DI_OK) { TRACE("CreateDevice(keyboard) failed"); return FALSE; }

r = keyboard->SetDataFormat( &c_dfDIKeyboard ); if (r!=DI_OK) { TRACE("keyboard->SetDataFormat() failed\n"); return FALSE; }

r=keyboard->SetCooperativeLevel( GetSafeHwnd(), DISCL_FOREGROUND | DISCL_NONEXCLUSIVE); if (r!=DI_OK) { TRACE("keyboard->SetCooperativeLevel() failed\n"); return FALSE; }

return TRUE; }


Инициализация клавиатуры происходит так же, как и в программе Qwerty.



Инициализация мыши


Функция InitMouse() (см. листинг 6.5) готовит мышь к работе.

Листинг 6.5. Функция InitMouse()


BOOL SmearWin::InitMouse() { HRESULT r; r = dinput->CreateDevice( GUID_SysMouse, &mouse, 0 ); if (r!=DI_OK) { TRACE("CreateDevice(mouse) failed\n"); return FALSE; }

r = mouse->SetDataFormat( &c_dfDIMouse ); if (r!=DI_OK) { TRACE("mouse->SetDataFormat() failed\n"); return FALSE; }

r = mouse->SetCooperativeLevel( GetSafeHwnd(), DISCL_NONEXCLUSIVE | DISCL_FOREGROUND ); if (r!=DI_OK) { TRACE("mouse->SetCooperativeLevel() failed\n"); return FALSE; }

DIPROPDWORD property; property.diph.dwSize=sizeof(DIPROPDWORD); property.diph.dwHeaderSize=sizeof(DIPROPHEADER); property.diph.dwObj=0; property.diph.dwHow=DIPH_DEVICE; property.dwData=64;

r = mouse->SetProperty( DIPROP_BUFFERSIZE, &property.diph ); if (r!=DI_OK) { TRACE("mouse->SetProperty() failed (buffersize)\n"); return FALSE; }

return TRUE; }


Функция InitMouse() включает в себя четыре этапа:

Создание объекта DirectInputDevice, представляющего мышь.

Определение формата данных, получаемых от мыши.

Установку уровня кооперации для мыши.

Инициализацию буфера данных устройства.

Функция CreateDevice() интерфейса DirectInput (первый этап) создает экземпляр интерфейса DirectInputDevice, представляющего системную мышь:


r = dinput->CreateDevice( GUID_SysMouse, &mouse, 0 );


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

Если вызов функции CreateDevice() прошел успешно, переменной mouse присваивается указатель на созданный объект DirectInputDevice. Третий аргумент должен быть равен нулю, если только вы не пользуетесь агрегированием COM.

На втором этапе функция SetDataFormat() интерфейса DirectInputDevice сообщает DirectInput формат ожидаемых данных:



r = mouse->SetDataFormat( &c_dfDIMouse );


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

На третьем этапе определяется уровень кооперации для мыши:

r = mouse->SetCooperativeLevel( GetSafeHwnd(), DISCL_NONEXCLUSIVE | DISCL_FOREGROUND );


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

Остается лишь задать размер буфера данных функцией SetProperty() интерфейса DirectInputDevice. Размер буфера определяет количество событий, сохраняемых в очереди DirectInput. Если буфер слишком мал, возникает риск потери данных из-за его переполнения. Я снова привожу соответствующий фрагмент
листинга 6.5:

DIPROPDWORD property; property.diph.dwSize=sizeof(DIPROPDWORD); property.diph.dwHeaderSize=sizeof(DIPROPHEADER); property.diph.dwObj=0; property.diph.dwHow=DIPH_DEVICE; property.dwData=64;

r = mouse->SetProperty( DIPROP_BUFFERSIZE, &property.diph );


Функция SetProperty() получает два аргумента: величину, которая определяет задаваемое свойство, и адрес структуры DIPROPDWORD. Среди прочего эта структура содержит значение свойства.

В нашем случае константа DIPROP_BUFFERSIZE говорит о том, что SetProperty() задает размер буфера. Поле dwSize структуры property равно 64; это значит, что мы заказываем буфер данных из 64 элементов. Размер буфера выбирается достаточно произвольно. Он должен быть достаточно большим, чтобы избежать переполнения, и достаточно малым, чтобы не тратить память напрасно.

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


Инициализация приложения


Наше знакомство с программой Switch начинается с конструктора switchWin, внутри которого происходит первоначальная инициализация переменных класса. Не следует путать эту инициализацию с той, что выполняется функцией CreateCustomSurfaces(), потому что в отличие конструктора CreateCustomSurfaces() вызывается при каждой смене видеорежима. Конструктор выглядит так:


SwitchWin::SwitchWin() { bmpsurf=0; x=y=0; xinc=8; yinc=1; menusurf=0; fpssurf=0;

vlargefont = CreateFont( 28, 0, 0, 0, FW_NORMAL, FALSE, FALSE, FALSE, ANSI_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS, DEFAULT_QUALITY, VARIABLE_PITCH, "Arial" );

smallfont = CreateFont( 14, 0, 0, 0, FW_NORMAL, FALSE, FALSE, FALSE, ANSI_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS, DEFAULT_QUALITY, VARIABLE_PITCH, "Arial" ); }


В основном конструктор просто обнуляет переменные. Два логических номера шрифтов инициализируются функцией Win32 CreateFont(). В программе используются два разных размера одного и того же шрифта: крупным шрифтом выводится заголовок на поверхности меню видеорежимов, а мелким — описания видеорежимов и текст со значением FPS.

После того как объект SwitchWin будет создан, DirectDrawWin вызывает функции SelectDriver() и SelectInitialDisplayMode(). Поскольку в программе Switch обе функции ведут себя стандартным образом (как описано в главе 3), мы не будем их рассматривать.

Затем класс DirectDrawWin вызывает функцию SwitchWin::CreateCustomSurfaces(), в которой подготавливает три поверхности, используемые программой Switch:


BOOL SwitchWin::CreateCustomSurfaces() { int displaydepth=GetCurDisplayDepth(); CString filename; if (displaydepth==8) filename="tri08.bmp"; else filename="tri24.bmp";

bmpsurf=CreateSurface( filename, TRUE ); if (bmpsurf==0) { TRACE("surface creation failed\n"); return FALSE; }

selectmode=GetCurDisplayMode(); CreateMenuSurface(); UpdateMenuSurface();

CreateFPSSurface();

return TRUE; }


Содержимое одной из этих трех поверхностей определяется BMP-файлом.

Как упоминалось выше, перед инициализацией DirectDraw программа SuperSwitch выводит в функции SuperSwitchWin::OnCreate() диалоговое окно. После вывода диалогового окна функция вызывает версию OnCreate() класса DirectDrawWin. Код функции SuperSwitchWin::OnCreate() выглядит так:


int SuperSwitchWin::OnCreate(LPCREATESTRUCT lpCreateStruct) { IntroDialog introdialog; if (introdialog.DoModal()!=IDOK) return -1;

include_refresh=introdialog.include_refresh;

if (DirectDrawWin::OnCreate(lpCreateStruct)==-1) return -1;

if (include_refresh) ddraw2->EnumDisplayModes( DDEDM_REFRESHRATES, 0, this, StoreModeInfo );

return 0; }


Сначала мы создаем объект класса IntroDialog — этот класс-оболочка был сгенерирован ClassWizard. Диалоговое окно отображается функцией CDialog::DoModal(), которая возвращает код IDOK в случае нажатия пользователем кнопки OK. Если пользователь закрывает диалоговое окно другим способом (например, нажимая кнопку Cancel), функция OnCreate() возвращает код –1, что для MFC является признаком завершения приложения. Если была нажата кнопка OK, переменной include_refresh присваивается значение в зависимости от состояния флажка в диалоговом окне.

Теперь мы вызываем версию OnCreate() класса DirectDrawWin, где и происходит инициализация DirectDraw. Функция составляет список видеорежимов, активизирует исходный режим и создает поверхности приложения. Если вызов функции OnCreate() завершается неудачей, мы завершаем приложение, возвращая код –1.

Следующий шаг - повторное составление списка видеорежимов. На этот раз при вызове функции EnumDisplayModes() в первом аргументе передается флаг DDEDM_REFRESHRATES, согласно которому каждый видеорежим должен быть включен в список по одному разу для каждой поддерживаемой частоты. В результате мы сможем построить список частот для каждого видеорежима. Четвертый аргумент EnumDisplayModes() — функция косвенного вызова StoreModeInfo(), которая выглядит так:


HRESULT WINAPI SuperSwitchWin::StoreModeInfo( LPDDSURFACEDESC desc, LPVOID p) { DWORD w=desc->dwWidth; DWORD h=desc->dwHeight; DWORD d=desc->ddpfPixelFormat.dwRGBBitCount; DWORD r=desc->dwRefreshRate;

SuperSwitchWin* win=(SuperSwitchWin*)p; int index=win->GetDisplayModeIndex( w, h, d ); win->refresh_rates[index].Add(r);

return DDENUMRET_OK; }


Функции StoreModeInfo()> передается указатель на структуру DDSURFACEDESC с описанием очередного видеорежима. В описание входит частота смены кадров (поле dwRefreshRate), а также размеры, по которым определяется индекс режима. Затем этот индекс используется для сохранения частоты видеорежима в массиве.

После выхода из функции OnCreate() класс DirectDrawWin вызывает функцию CreateCustomSurfaces(). По сравнению с программой Switch эта функция не изменилась; она по-прежнему создает три поверхности, потому что новая поверхность (ratemenusurface) создается только в случае необходимости.




Перед тем как инициализировать DirectDraw, класс DirectDrawWin вызывает функцию SelectDriver(), чтобы производные классы могли выбрать драйвер DirectDraw при наличии нескольких вариантов. В программе BmpView мы отказываемся от этой возможности и позволяем выбрать первичный драйвер по умолчанию. Это сделано потому, что для вывода диалоговых окон используется механизм GDI, а GDI может выводить только на первичное видеоустройство (которому соответствует первичный драйвер DirectDraw).

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

Листинг 5.6. Функция BmpViewWin::SelectInitialDisplayMode()


int BmpViewWin::SelectInitialDisplayMode() { DisplayModeDescription desc; int i, nummodes=GetNumDisplayModes(); DWORD w,h,d;

for (i=0;i<nummodes;i++) { GetDisplayModeDimensions( i, w, h, d ); desc.w=w; desc.h=h; desc.d=d; desc.desc.Format("%dx%dx%d", w, h, d );

if ( d==8 ) palettemode.Add( desc ); else nonpalettemode.Add( desc ); }

DWORD curdepth=GetDisplayDepth();

for (i=0;i<nummodes;i++) { GetDisplayModeDimensions( i, w, h, d ); if (w==640 && h==480 && d==curdepth) return i; }

for (i=0;i<nummodes;i++) { GetDisplayModeDimensions( i, w, h, d ); if (d==curdepth) return i; }

for (i=0;i<nummodes;i++) { GetDisplayModeDimensions( i, w, h, d ); if (w==640 && h==480) return i; }

GetSystemPalette();

return 0; }


Помимо выбора исходного видеорежима функция SelectInitialDisplayMode() используется для подготовки двух массивов: в первом хранятся сведения о палитровых (palettemode), а во втором — о беспалитровых (nonpalettemode) видеорежимах. Мы воспользуемся этими массивами позднее, при отображении диалогового окна.


Наше знакомство с программой Cursor начинается с функции OnCreate(), которая отвечает за инициализацию DirectDraw, DirectInput и потока ввода. Функция OnCreate() приведена в листинге 7.2.

Листинг 7.2. Функция CursorWin::OnCreate()


int CursorWin::OnCreate(LPCREATESTRUCT lpCreateStruct) { HRESULT r=DirectInputCreate( AfxGetInstanceHandle(), DIRECTINPUT_VERSION, &dinput, 0 ); if (r!=DI_OK) { AfxMessageBox("DirectInputCreate() failed"); return -1; }

if (InitMouse()==FALSE) return -1;

if (InitKeyboard()==FALSE) return -1;

if (DirectDrawWin::OnCreate(lpCreateStruct) == -1) return -1;

mousethread->ResumeThread();

return 0; }


Сначала OnCreate() инициализирует DirectInput функцией DirectInputCreate(). Затем мышь и клавиатура инициализируются функциями InitMouse() и InitKeyboard(), после чего вызывается функция DirectDrawWin::OnCreate(). Функция InitMouse(), которую мы рассмотрим чуть ниже, создает поток ввода, доступ к которому осуществляется через указатель mousepointer. Однако поток ввода создается в приостановленном состоянии, чтобы он не пытался преждевременно обращаться к первичной поверхности. Поток будет запущен лишь после инициализации DirectDraw. Приостановленный поток активизируется функцией CWinThread::ResumeThread().

Давайте рассмотрим функцию InitMouse(), чтобы получить общее представление об инициализации мыши и создании потока ввода. Функция InitMouse() приведена в листинге 7.3.

Листинг 7.3. Функция InitMouse()


BOOL CursorWin::InitMouse() { HRESULT r; r = dinput->CreateDevice( GUID_SysMouse, &mouse, 0 ); if (r!=DI_OK) { TRACE("CreateDevice(mouse) failed\n"); return FALSE; }

r = mouse->SetDataFormat( &c_dfDIMouse ); if (r!=DI_OK) { TRACE("mouse->SetDataFormat() failed\n"); return FALSE; }

r = mouse->SetCooperativeLevel( GetSafeHwnd(), DISCL_NONEXCLUSIVE | DISCL_FOREGROUND ); if (r!=DI_OK) { TRACE("mouse->SetCooperativeLevel() failed\n"); return FALSE; }

DIPROPDWORD property; property.diph.dwSize=sizeof(DIPROPDWORD); property.diph.dwHeaderSize=sizeof(DIPROPHEADER); property.diph.dwObj=0; property.diph.dwHow=DIPH_DEVICE; property.dwData=64;

r = mouse->SetProperty( DIPROP_BUFFERSIZE, &property.diph ); if (r!=DI_OK) { TRACE("mouse->SetProperty() failed (buffersize)\n"); return FALSE; }

mouse_event[mouse_event_index]=new CEvent; mouse_event[quit_event_index]=new CEvent;

r = mouse->SetEventNotification( *mouse_event[mouse_event_index] ); if (r!=DI_OK) { TRACE("mouse->SetEventNotification() failed\n"); return FALSE; } mousethread=AfxBeginThread( (AFX_THREADPROC)MouseThread, this, THREAD_PRIORITY_TIME_CRITICAL, 0, CREATE_SUSPENDED );

return TRUE; }

<


При запуске программы Bumper прежде всего вызывается функция SelectDriver(). Чтобы добиться максимальной гибкости, при наличии нескольких драйверов DirectDraw программа Bumper выводит меню. Функция SelectDriver() выглядит так:


int BumperWin::SelectDriver() { int numdrivers=GetNumDrivers(); if (numdrivers==1) return 0; CArray<CString, CString> drivers; for (int i=0;i<numdrivers;i++) { LPSTR desc, name; GetDriverInfo( i, 0, &desc, &name ); drivers.Add(desc); }

DriverDialog dialog; dialog.SetContents( &drivers ); if (dialog.DoModal()!=IDOK) return -1;

return dialog.GetSelection(); }


С помощью класса DriverDialog

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

Функция SelectInitialDisplayMode() вызывается после функции SelectDriver(), но перед созданием поверхностей. Функция выглядит так:


int BumperWin::SelectInitialDisplayMode() { DWORD curdepth=GetDisplayDepth(); int i, nummodes=GetNumDisplayModes(); DWORD w,h,d; if (curdepth!=desireddepth) ddraw2->SetDisplayMode( 640, 480, curdepth, 0, 0 );

for (i=0;i<nummodes;i++) { GetDisplayModeDimensions( i, w, h, d ); if (w==desiredwidth && h==desiredheight && d==desireddepth) return i; }

ddraw2->RestoreDisplayMode(); ddraw2->Release(), ddraw2=0; AfxMessageBox("Can't find 8-bit mode on this device");

return -1; }


Функция SelectInitialDisplayMode()

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

Интерфейс DirectDrawClipper


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

Экземпляры интерфейса DirectDrawClipper создаются функцией CreateClipper() интерфейса DirectDraw. Интерфейс DirectDrawClipper содержит следующие функции: SetHWnd()

GetHWnd()

IsClipListChanged()

SetClipList()

GetClipList()

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

Отсечение для рабочего стола активизируется функцией SetHWnd(). Функция SetHWnd() присоединяет объект отсечения к логическому номеру (handle) окна. В результате инициируется взаимодействие Windows с объектом отсечения. Объект отсечения получает уведомления обо всех изменениях окон на рабочем столе и действует соответствующим образом. Функция GetHWnd() определяет, к какому логическому номеру окна присоединен заданный объект отсечения (и присоединен ли он вообще). Функция IsClipListChanged() определяет, был ли внутренний список отсечений изменен вследствие изменений на рабочем столе.

Функции SetClipList() и GetClipList() упрощают нестандартное использование интерфейса DirectDrawClipper. Функция SetClipList() определяет набор прямоугольных областей, для которых разрешено выполнение блиттинга. Функция GetClipList() извлекает внутренние данные объекта отсечения.

После того как экземпляр DirectDrawClipper будет присоединен к поверхности, происходит автоматическое отсечение операций блиттинга, выполняемых функциями Blt(), BltBatch() и UpdateOverlay(). Обратите внимание на то, что в список не входит функция BltFast(). Для нее отсечение не поддерживается.



Интерфейс DirectDrawPalette


Интерфейс DirectDrawPalette предназначен для работы с палитровыми видеорежимами и поверхностями. Несмотря на то что в Windows поддерживается ряд видеорежимов с глубиной пикселей менее 8 бит, DirectDraw поддерживает лишь 8-битные палитровые режимы.

Экземпляры интерфейса DirectDrawPalette создаются функцией CreatePalette() интерфейса DirectDraw. Функция CreatePalette() получает набор флагов, определяющих тип палитры.

Интерфейс DirectDrawPalette содержит всего три функции:

GetCaps()

GetEntries()

SetEntries()

Функция GetCaps() определяет возможности палитры. В числе получаемых сведений — количество элементов палитры, поддержка палитрой вертикальной синхронизации и (в случае 8-битной палитры) возможность заполнения всех 256 элементов.

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

Экземпляры интерфейса DirectDrawPalette присоединяются к поверхности функцией SetPalette() интерфейса DirectDrawSurface. Палитровая анимация выполняется либо присоединением разных палитр к первичной поверхности, либо изменением содержимого палитры функцией SetEntries().



Интерфейс DirectInput


Инициализация DirectInput происходит в тот момент, когда вы получаете указатель на интерфейс DirectInput функцией DirectInputCreate(). Затем полученным интерфейсом можно воспользоваться для создания экземпляров интерфейса DirectInputDevice, составления списка доступных устройств и даже для вызова панели управления DirectInput. Интерфейс DirectInput содержит следующие четыре функции:

CreateDevice()

EnumDevice()

GetDeviceStatus()

RunControlPanel()

Функция CreateDevice() создает новые экземпляры интерфейса DirectInputDevice. Она получает три аргумента: GUID нужного устройства, адрес инициализируемого указателя на интерфейс и показатель агрегирования (aggregation) COM, который обычно должен быть равен нулю. Для системной мыши и клавиатуры в DirectX предусмотрены стандартные значения GUID:

GUID_SysKeyboard

GUID_SysMouse

Значения GUID остальных устройств можно получить функцией EnumDevices().

Функция EnumDevices() обнаруживает устройства ввода, установленные на данном компьютере. Она позволяет составить список устройств по их типу, по факту их текущего подключения к компьютеру и по тому, являются ли они устройствами с обратной связью. Для каждого устройства, обнаруженного функцией EnumDevices(), предоставляются два значения GUID: GUID экземпляра и GUID продукта. GUID экземпляра идентифицирует конкретное устройство, а GUID продукта — его тип. При вызове функции CreateDevice() используется GUID экземпляра.

С помощью функции GetDeviceStatus() можно определить, доступно ли устройство для DirectInput в данный момент. Код возврата DI_OK означает, что устройство подключено и доступно. Функция RunControlPanel() вызывает приложение DirectInput Control Panel. Точнее, она вызывает приложение DirectX Control Panel и активизирует вкладку DirectInput.



Интерфейс DirectInputDevice


Доступ ко всем устройствам ввода, представленным в DirectInput, осуществляется через интерфейс DirectInputDevice. Интерфейс DirectInputDevice содержит следующие функции:

Acquire()

Unacquire()

GetCapabilities()

GetDeviceData()

GetDeviceInfo()

GetDeviceState()

SetDataFormat()

SetEventNotification()

EnumObjects()

GetObjectInfo()

GetProperty()

SetProperty()

SetCooperativeLevel()

RunControlPanel()

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

Функции Acquire() и Unacquire() устанавливают и разрывают связь между устройством ввода и DirectInput. Перед тем как получать от устройства данные, необходимо захватить его.

Функция Acquire() используется для начального захвата устройства и для восстановления нарушенной связи с устройством. Обычно связь между устройством ввода и DirectInput разрывается из-за того, что приложение теряет фокус ввода. Функция Unacquire() используется, как правило, для того, чтобы вернуть Windows право доступа к устройству. Например, при работе с меню необходимо уступить объекты DirectInputDevice, представляющие мышь и клавиатуру, чтобы Windows могли нормально обработать выбор команды меню.

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

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

Функция GetDeviceInfo() заполняет структуру DIDEVICEINSTANCE информацией об устройстве. В структуру заносятся значения GUID экземпляра и продукта для данного устройства, а также строки с неформальными описаниями.
Ту же самую информацию можно получить функцией EnumDevices() интерфейса DirectInput, так что, строго говоря, эта функция не является обязательной.

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

Функция SetDataFormat() описывает формат, в котором вводимые данные возвращаются устройством. Приложение может определить собственный формат, а в DirectInput предусмотрены три стандартных формата для стандартных устройств:

c_dfDIKeyboard

c_dfDIMouse

c_dfDIJoystick


Перед захватом устройства необходимо вызвать для него функцию SetDataFormat().

Функция SetEventNotification(), особенно часто используемая в многопоточных приложениях, предназначена для обработки оповещений. При изменении состояния данного устройства DirectInput сигнализирует о наступлении события. Таким образом, поток может перейти в режим блокировки (например, с помощью функции WaitForSingleObject()) и ожидать изменений в устройстве ввода. При наступлении события поток автоматически отреагирует на него.

Каждое устройство ввода содержит один или несколько объектов. Для клавиатуры объект представляет отдельную клавишу. Для мыши объекты представляют каждую кнопку и каждую ось. Функция EnumObjects() составляет список объектов заданного устройства и возвращает для каждого объекта GUID и строку. Строка содержит неформальное описание объекта (например, «ось X» или «Right Shift»). GUID описывает тип объекта и может принимать одно из следующих значений:

GUID_XAxis

GUID_YAxis

GUID_ZAxis

GUID_RAxis

GUID_UAxis

GUID_VAxis

GUID_Button

GUID_Key

GUID_POV


Чтобы успокоить вас, скажу, что функция EnumObjects() обычно не нужна, особенно для стандартных устройств. Например, для работы с клавиатурой вам не придется составлять список всех клавиш.

Функция GetObjectInfo() позволяет получить ту же информацию, что и EnumObjects(), но без предварительного составления списка объектов.


Интересующий вас объект задается значением его смещения или идентификатора.

Функции GetProperty() и SetProperty() применяются для просмотра и установки параметров устройств (свойств), отсутствующих в DirectInput API. В DirectInput предусмотрен ряд стандартных свойств (например, свойства autocenter и deadzone для джойстиков), однако эти функции могут применяться и для других, нестандартных свойств.

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

Функция SetCooperativeLevel() определяет степень контроля над заданным устройством. Допускаются следующие значения:

DISCL_BACKGROUND

DISCL_EXCLUSIVE

DISCL_FOREGROUND

DISCL_NONEXCLUSIVE


Вызов функции SetCooperativeLevel() наряду с вызовом SetDataFormat() необходим для получения данных от устройства.

Наконец, функция RunControlPanel() запускает приложение Control Panel для типа устройства, представленного интерфейсом DirectInputDevice. Например, для стандартной клавиатуры эта функция запускает приложение Keyboard, которое также можно запустить из стандартной Control Panel. Не путайте ее с функцией RunControlPanel() интерфейса DirectInput, которая запускает общее приложение DirectInput Control Panel вместо приложения для конкретного устройства ввода.




В первоначальном варианте библиотеки DirectX


В первоначальном варианте библиотеки DirectX (еще в те времена, когда она называлась Game SDK) вся основная функциональность DirectDraw была сосредоточена в интерфейсе DirectDraw. Позднее, с выходом DirectX 2, рабочий интерфейс был усовершенствован. В соответствии со спецификацией COM интерфейс DirectDraw не изменился, а для работы с новыми возможностями использовался новый интерфейс DirectDraw2. Следует заметить, что интерфейс DirectDraw2 представляет собой расширение DirectDraw. Он предоставляет все возможности интерфейса DirectDraw, а также ряд дополнительных. При работе с DirectX версий 2 и выше можно выбирать между интерфейсом DirectDraw и DirectDraw2. Поскольку DirectDraw2 делает все то же, что и DirectDraw, а также многое другое, вряд ли можно найти какие-то доводы в пользу работы с DirectDraw. Кроме того, Microsoft выступает против хаотичного, непоследовательного использования этих интерфейсов. По этим причинам во всех программах, приведенных в книге, будет использован интерфейс DirectDraw2.

Ниже перечислены все функции интерфейсов DirectDraw и DirectDraw2 (в алфавитном порядке):

Compact()

CreateClipper()

CreatePalette()

CreateSurface()

DuplicateSurface()

EnumDisplayModes()

EnumSurfaces()

FlipToGDISurface()

GetAvailableVidMem()

GetCaps()

GetDisplayMode()

GetFourCCCodes()

GetGDISurface()

GetMonitorFrequency()

GetScanLine()

GetVerticalBlankStatus()

RestoreDisplayMode()

SetCooperativeLevel()

SetDisplayMode()

WaitForVerticalBlank()


Далее рассмотрены функции интерфейса DirectDraw. Обратите внимание на то, что в оставшейся части этой главы термин интерфейс DirectDraw относится как к интерфейсу DirectDraw, так и к DirectDraw2. Уточнения будут приведены лишь в тех случаях, когда функция отличается в двух интерфейсах.


Интерфейсы DirectDrawSurface


Множественные интерфейсы DirectDrawSurface, как и интерфейсы DirectDraw, возникли из-за особенностей спецификации COM. В исходном варианте работа с поверхностями осуществлялась через интерфейс DirectDrawSurface. В DirectX 2 появились новые функциональные возможности, представленные интерфейсом DirectDrawSurface2, а в DirectX 5 возник интерфейс DirectDrawSurface3.

Хотя в этой книге вместо DirectDraw повсюду используется интерфейс DirectDraw2, для работы с поверхностями мы будем придерживаться исходного интерфейса DirectDrawSurface, потому что нововведения интерфейсов DirectDrawSurface2 и DirectDrawSurface3 не слишком важны. В оставшейся части книги термин интерфейс DirectDrawSurface будет обозначать все три интерфейса, если при этом не возникает двусмысленности.

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

AddAttachedSurface()

AddOverlayDirtyRect()

Blt()

BltBatch()

BltFast()

DeleteAttachedSurface()

EnumAttachedSurfaces()

EnumOverlayZOrders

Flip

GetAttachedSurface()

GetBltStatus()

GetCaps()

GetClipper()

GetColorKey()

GetDC()

GetDDInterface()

GetFlipStatus()

GetOverlayPosition()

GetPalette()

GetPixelFormat()

GetSurfaceDesc()

IsLost()

Lock()

PageLock()

PageUnlock()

ReleaseDC()

Restore()

SetClipper()

SetColorKey()

SetOverlayPosition()

SetPalette()

SetSurfaceDesc()

Unlock()

UpdateOverlay()

UpdateOverlayDisplay()

UpdateOverlayZOrder()



Эмуляция версий


Одна из широко разрекламированных возможностей COM— управление версиями. COM-объекты устроены так, что доступ к ним может осуществляться только через строго определенные интерфейсы. В соответствии с правилами COM, интерфейс не может изменяться после его определения. Вместо этого приходится вводить новый интерфейс, который поддерживает как старые, так и новые возможности. При этом новые программы могут без опасений пользоваться новыми возможностями, а старые — работать со старыми интерфейсами, которые заведомо не изменятся. Эта схема неплохо работает и помогает обеспечить совместимость приложений DirectX со старыми и новыми runtime-частями библиотеки.

К сожалению, в DirectX API часто используются структуры. Эти структуры являются «открытыми» — доступ к ним осуществляется непосредственно, а не через интерфейс, как для COM-объектов. Размер этих структур может изменяться (и часто изменяется) при переходе к новой версии DirectX. По этой причине каждая функция DirectX, которой в качестве аргумента передается указатель на структуру, должна обязательно получать и размер передаваемой структуры. Благодаря этому runtime-часть DirectX всегда может узнать, какая версия DirectX SDK применялась для компиляции приложения, и следовательно — какие поля входят в структуру. Проблема решена, не так ли?

А что вы скажете насчет программы, которая была откомпилирована в DirectX 5 SDK, но затем запущена с runtime-частью DirectX 3? Если одна или несколько структур DirectX 5 были дополнены новыми полями и флагами, runtime-часть не сможет обработать эту структуру, потому что ничего не знает о появившихся в ней расширениях.

К решению этой проблемы (которую мы ласково назовем «структурной ошибкой DirectX») можно подойти четырьмя способами:

поставлять нужную runtime-часть DirectX вместе с продуктом и настаивать на том, чтобы она устанавливалась на компьютерах со старыми версиями;

написать «умный» код, который проверяет версию установленных DLL и затем использует только структуры, поддерживаемые runtime-частью;



выбрать самую старую версию runtime-части, поддерживаемую вашей программой, и написать код в расчете на нее;

отказаться от технологии и всего, что с ней связано.


Коммерческие программы (особенно пакеты, распространяемые на CD-ROM) в основном используют первый вариант. Он прост и позволяет всегда работать с новейшими возможностями DirectX. С другой стороны, runtime-часть DirectX 5 занимает 135 Мбайт. Это значит, что для настоящего продукта на CD-ROM остается меньше места, или вам придется поставлять дополнительный диск с DirectX. Разумеется, этот вариант не подходит для приложений, распространяемых без CD-ROM или через Internet.

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

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

Каждый компонент DirectX определяет номер версии и пользуется им в заголовочном файле. Для DirectDraw этой цели служит символическая константа DIRECTDRAW_VERSION. Например, в DirectX 3 SDK константа DIRECTDRAW_VERSION равна 0300 (завершающие нули обозначают младший номер версии).

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

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

#define DIRECTDRAW_VERSION 0x300 #include <ddraw.h>

то определяемые структуры будут идентичны тем, что определялись в DirectX 3 SDK, даже если на самом деле вы работаете с DirectX 5 SDK.

Вариант 4 выглядит соблазнительно (но я слишком люблю играть в Quake).


Класс AviPlayWin


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

использует диалоговое окно для выбора файла. Вместо того чтобы создавать поверхности при запуске, программа AviPlay (как и программа BmpView из главы 5) ожидает, пока пользователь выберет файл. Затем программа создает поверхности и настраивает их в соответствии с содержимым выбранного файла. Определение класса AviPlayWin приведено в листинге 8.1.

Листинг 8.1. Класс AviPlayWin


class AviPlayWin : public DirectDrawWin { public: AviPlayWin(); protected: //{{AFX_MSG(AviPlayWin) afx_msg void OnKeyDown(UINT nChar, UINT nRepCnt, UINT nFlags); afx_msg void OnRButtonDown(UINT nFlags, CPoint point); afx_msg int OnCreate(LPCREATESTRUCT lpCreateStruct); afx_msg void OnDestroy(); //}}AFX_MSG DECLARE_MESSAGE_MAP() private: int SelectInitialDisplayMode(); BOOL CreateCustomSurfaces() { return TRUE; } void DrawScene(); void RestoreSurfaces(); void GetSystemPalette(); void ShowDialog(); BOOL LoadAvi(); BOOL CreateAviSurface(); BOOL UpdateAviSurface(); BOOL InstallPalette(); private: AviDialog* avidialog;

CString fullfilename; CString filename; CString pathname;

CRect displayrect; LPDIRECTDRAWSURFACE avisurf; CRect avirect; int x,y; DisplayModeArray displaymode;

LPDIRECTDRAWPALETTE syspal; LPDIRECTDRAWPALETTE avipal;

PAVISTREAM avistream; AVISTREAMINFO streaminfo; HIC decomp; long fmtlen, buflen; long startframe, endframe; long curframe; LPBITMAPINFOHEADER srcfmt; LPBITMAPINFOHEADER dstfmt; BYTE* rawdata; BYTE* finaldata; };


Сначала мы объявляем конструктор класса AviPlayWin, предназначенный только для инициализации переменных класса.

В классе определены четыре обработчика сообщений: OnKeyDown(), OnRButtonDown(), OnCreate() и OnDestroy(). Функция OnKeyDown() следит за нажатием клавиш Escape

и пробела во время воспроизведения, прерывает ролик и отображает диалоговое окно для выбора AVI-файла (мы могли воспользоваться DirectInput, но программа AviPlay не стоит подобных хлопот).
Функция OnRButtonDown() тоже вызывает диалоговое окно для выбора AVI-файла, но по щелчку правой кнопки мыши. Функция OnCreate() инициализирует DirectDraw и AVI, а функция OnDestroy() завершает их работу.

Затем мы объявляем 10 закрытых (private) функций. Первой идет функция SelectInitialDisplayMode(), которая выполняет три задачи: выбор исходного видеорежима (то, для чего предназначена сама функция), построение списка 8-битных режимов для диалогового окна и захват системной палитры. Вскоре мы рассмотрим эту функцию. Функция GetSystemPalette() вызывается функцией SelectInitialDisplayMode(); мы увидим, как она работает, при знакомстве с последней.

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

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

За функций LoadAvi() объявляется функция DrawScene(). Мы воспользуется ею для вывода кадров видеоролика. Помимо извлечения и восстановления кадров видеопотока DrawScene() осуществляет блиттинг и переключение страниц, необходимые для отображения кадра.

Функция RestoreSurfaces()

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

Функции CreateAviSurface() и UpdateAviSurface()

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

Последней объявлена функция InstallPalette(), которая устанавливает палитру AVI перед началом воспроизведения ролика.Однако перед этим она должна извлечь данные палитры из потока AVI.

Оставшаяся часть класса содержит лишь переменные. Мы познакомимся с ними во время рассмотрения программы.


Класс BumperWin


Программа Bumper, как и все остальные программы в этой книге, построена на основе базового класса DirectDrawWin. Производный от него класс BumperWin определяется так:


Рис. 9.4. Программа Bumper


class BumperWin : public DirectDrawWin { public: BumperWin(); protected: //{{AFX_MSG(BumperWin) afx_msg void OnKeyDown(UINT nChar, UINT nRepCnt, UINT nFlags); afx_msg void OnDestroy(); //}}AFX_MSG DECLARE_MESSAGE_MAP() private: int SelectDriver(); int SelectInitialDisplayMode(); BOOL CreateCustomSurfaces(); void DrawScene(); void RestoreSurfaces(); BOOL SpritesCollide(Sprite* s1, Sprite* s2); BOOL SpritesCollideRect(Sprite* s1, Sprite* s2); BOOL SpritesCollidePixel(Sprite* s1, Sprite* s2);

private: Sprite* sprite[MAX_SPRITES]; int nsprites;

LPDIRECTDRAWSURFACE text; };


В нем объявляются два обработчика сообщений. Функция OnKeyDown() обрабатывает нажатия клавиш, а функция OnDestroy() освобождает спрайты в конце работы программы.

Функции SelectDriver(), SelectInitialDisplayMode(), CreateCustomSurfaces(), DrawScene() и RestoreSurfaces() наследуются от класса DirectDrawWin. Вскоре мы подробно рассмотрим каждую из этих функций. Функции SpritesCollide(), SpritesCollideRect() и SpritesCollidePixel() совпадают с одноименными функциями, описанными выше, однако на этот раз они принадлежат классу BumperWin. Поскольку эти функции уже рассматривались, мы не будем обсуждать их снова.

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



Класс CursorWin


Программа Cursor, как и все остальные программы этой книги, построена на базе структурных классов DirectDrawWin и DirectDrawApp. Эти классы остались неизменными, а вся специфика приложения реализуется классом CursorWin. На практике функциональность курсора мыши, вероятно, следовало бы встроить в структурный класс. И все же для наглядности я объединил код для работы с курсором со специфическим кодом приложения. Класс CursorWin приведен в листинге7.1.

Листинг 7.1. Класс CursorWin


class CursorWin : public DirectDrawWin { public: CursorWin(); protected: //{{AFX_MSG(CursorWin) afx_msg int OnCreate(LPCREATESTRUCT lpCreateStruct); afx_msg void OnDestroy(); afx_msg void OnActivate(UINT nState, CWnd* pWndOther, BOOL bMinimized); //}}AFX_MSG DECLARE_MESSAGE_MAP() private: int SelectDriver(); int SelectInitialDisplayMode(); BOOL CreateCustomSurfaces(); void DrawScene(); void RestoreSurfaces(); private: BOOL InitMouse(); BOOL InitKeyboard(); BOOL UpdateDelaySurface(); private: //------- Функции потока ввода ------ static DWORD MouseThread(LPVOID); BOOL UpdateCursorSimpleCase(int curx, int cury, int oldcurx, int oldcury); BOOL UpdateCursorComplexCase(int curx, int cury, int oldcurx, int oldcury); private: //------- Данные мыши ------- static LPDIRECTINPUTDEVICE mouse;

static CCriticalSection critsection; static CWinThread* mousethread; static CEvent* mouse_event[2];

static int cursor_width; static int cursor_height; static LPDIRECTDRAWSURFACE cursor; static LPDIRECTDRAWSURFACE cursor_under; static LPDIRECTDRAWSURFACE cursor_union;

static int curx, cury; static int oldcurx, oldcury; static CList<MouseClickData, MouseClickData> mouseclickqueue; private: //------- Данные приложения ------- LPDIRECTINPUT dinput; LPDIRECTINPUTDEVICE keyboard;

LPDIRECTDRAWSURFACE coil[coil_frames];

LPDIRECTDRAWSURFACE dm_surf; int dm_index;

DWORD menubarfillcolor; HFONT largefont, smallfont; };


Класс CursorWin объявляет три обработчика сообщений: OnCreate(), OnDestroy() и OnActivate().
Функция OnCreate() инициализирует DirectDraw, DirectInput и поток ввода. Функция OnDestroy() освобождает интерфейсы DirectX и завершает поток ввода. Функция OnActivate() обеспечивает захват мыши и клавиатуры на период активности приложения.

Следующие пять функций наследуются от класса DirectDrawWin:

SelectDriver()

SelectInitialDisplayMode()

CreateCustomSurfaces()

DrawScene()

RestoreSurfaces()


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

Затем объявляются функции InitMouse() и InitKeyboard(). Эти функции используются функцией OnCreate() и отвечают за инициализацию объектов DirectInput, предназначенных для работы с мышью и клавиатурой. Функция InitKeyboard() совпадает с одноименными функциями программ Qwerty и Smear из главы 6, поэтому она также не рассматривается. Однако функция InitMouse() помимо инициализации мыши запускает поток ввода. Вскоре мы рассмотрим эту функцию.

Функция UpdateDelaySurface() готовит к выводу поверхность меню задержки. Она выводит текст меню и выделяет текущую задержку.

Далее в классе CursorWin объявляются три функции потока мыши:

MouseThread()

UpdateCursorSimpleCase()

UpdateCursorComplexCase()


Функция MouseThread() реализует поток ввода. Когда основной поток создает поток ввода, он передает указатель на статическую функцию MouseThread(). Созданный поток использует эту функцию в качестве точки входа и продолжает выполнять ее до возврата из функции или вызова функции AfxEndThread(). Функция MouseThread() обновляет изображение курсора с помощью функций UpdateCursorSimpleCase() и UpdateCursorComplexCase().

В оставшейся части класса CursorWin

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

Обратите внимание: в число переменных мыши входят объекты классов CCriticalSection, CEvent и CWinThread, предназначенные для синхронизации двух потоков нашей программы.

Мы объявляем два указателя на объекты CEvent — один используется для оповещений DirectInput, а второй сигнализирует о завершении потока.

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


Класс QwertyWin


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

Листинг 6.1. Класс QwertyWin


class QwertyWin : public DirectDrawWin { public: QwertyWin(); protected: //{{AFX_MSG(QwertyWin) afx_msg int OnCreate(LPCREATESTRUCT lpCreateStruct); afx_msg void OnDestroy(); afx_msg void OnActivate(UINT nState, CWnd* pWndOther, BOOL bMinimized); //}}AFX_MSG DECLARE_MESSAGE_MAP() private: int SelectDriver(); int SelectInitialDisplayMode(); BOOL CreateCustomSurfaces(); void DrawScene(); void RestoreSurfaces(); private: LPDIRECTINPUT dinput; LPDIRECTINPUTDEVICE keyboard;

BOOL esc_pressed; LPDIRECTDRAWSURFACE esc_up, esc_dn;

LPDIRECTDRAWSURFACE space_up, space_dn;

LPDIRECTDRAWSURFACE q_up, q_dn; LPDIRECTDRAWSURFACE w_up, w_dn; LPDIRECTDRAWSURFACE e_up, e_dn; LPDIRECTDRAWSURFACE r_up, r_dn; LPDIRECTDRAWSURFACE t_up, t_dn; LPDIRECTDRAWSURFACE y_up, y_dn;

LPDIRECTDRAWSURFACE rctrl_up, rctrl_dn; LPDIRECTDRAWSURFACE lctrl_up, lctrl_dn; LPDIRECTDRAWSURFACE lalt_up, lalt_dn; LPDIRECTDRAWSURFACE ralt_up, ralt_dn;

};


Прежде чем двигаться дальше, обратите внимание на отсутствие обработчика OnKeyDown(). Во всех программах, рассмотренных нами ранее, функция OnKeyDown() обрабатывала сообщения от клавиатуры. В программе Qwerty мы пользуемся услугами DirectInput и потому не нуждаемся в OnKeyDown().

В самом начале объявляются три обработчика сообщений:

OnCreate()

OnDestroy()

OnActivate()

Функция OnCreate() инициализирует и настраивает DirectInput, а функция OnDestroy() освобождает объекты DirectInput. Функция OnActivate(), вызываемая MFC при получении или потере фокуса, будет использована для повторного захвата клавиатуры.

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


Функции CreateCustomSurfaces() и RestoreSurfaces() делают то же, что и раньше, так что они тоже не рассматриваются. Достаточно сказать, что эти функции инициализируют и восстанавливают поверхности, указатели на которые объявляются в нижней части
листинга 6.1.

Функция DrawScene() с помощью DirectInput определяет, какие клавиши были нажаты, и обеспечивает соответствующий вывод. Вскоре мы рассмотрим эту функцию.

После функций следуют переменные класса. Сначала объявляется указатель на интерфейс DirectInput (dinput), через него выполняется инициализация и осуществляются обращения к DirectInput. Переменная key — указатель на интерфейс DirectInputDevice, используемый для обращений к клавиатуре. Логическая переменная esc_pressed сигнализирует о завершении приложения.

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


Класс SmearWin


Основная функциональность программы Smear обеспечивается классом SmearWin (см. листинг 6.4).

Листинг 6.4. Класс SmearWin


class SmearWin : public DirectDrawWin { public: SmearWin(); protected: //{{AFX_MSG(SmearWin) afx_msg int OnCreate(LPCREATESTRUCT lpCreateStruct); afx_msg void OnDestroy(); afx_msg void OnActivate(UINT nState, CWnd* pWndOther, BOOL bMinimized); //}}AFX_MSG DECLARE_MESSAGE_MAP() private: BOOL CreateFlippingSurfaces(); private: int SelectDriver(); int SelectInitialDisplayMode(); BOOL CreateCustomSurfaces(); void DrawScene(); void RestoreSurfaces(); private: BOOL InitKeyboard(); BOOL InitMouse(); private: LPDIRECTINPUT dinput; LPDIRECTINPUTDEVICE mouse; LPDIRECTINPUTDEVICE keyboard;

LPDIRECTDRAWSURFACE sphere; int x, y; };


В классе объявлены три обработчика:

OnCreate()

OnDestroy()

OnActivate()

Функция OnCreate() инициализирует DirectInput, а также готовит к работе мышь и клавиатуру. Функция OnDestroy() освобождает объекты DirectInput, инициализированные функцией OnCreate(). Функция OnActivate() захватывает клавиатуру в начале работы и при повторной активизации приложения.

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

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

Затем класс SmearWin объявляет функции InitMouse() и InitKeyboard(). Функция OnCreate() возлагает на них ответственность за инициализацию устройств.

Наконец, мы объявляем несколько переменных. Переменная dinput — указатель на интерфейс DirectInput, она используется для работы с DirectInput после инициализации. Переменные mouse и keyboard указывают на интерфейсы DirectInputDevice, они инициализируются функциями InitMouse() и InitKeyboard() соответственно. Указатель на поверхность sphere и целые переменные x и y предназначены для вывода и позиционирования единственной поверхности приложения.



Класс Sprite


В коде предыдущего раздела класс Sprite использовался для представления спрайтов, проверяемых на столкновение. Давайте посмотрим, как он реализован.

Как мы уже видели, класс Sprite

содержит ряд функций, с помощью которых при проверке столкновений можно получить сведения о каждом спрайте. В частности, функция GetRect() возвращает контурный прямоугольник спрайта, а функция GetSurf() — поверхность, на которой находится спрайт. Однако класс Sprite

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

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

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

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

Чтобы избежать подобных неприятностей, необходимо соблюдать два правила:

Положение спрайтов не должно изменяться до завершения цикла проверок.

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




Класс Sprite эти правила соблюдает и, следовательно, справляется со всеми проблемами. Для этого обработка каждого столкновения осуществляется за две стадии, которые мы назовем подтверждением (acknowledgment) и реакцией

(reaction). На стадии подтверждения спрайт всего лишь сохраняет статус и положение другого спрайта— его собственное положение и статус остаются неизменными. Затем, на стадии реакции, по ранее сохраненным данным определяются дальнейшие действия, вызванные столкновением. На этой стадии положение и статус спрайта могут изменяться. Функция Hit() класса Sprite используется для подтверждения, а функция Update() — для реакции. Класс Sprite определяется так:

class Sprite { public: Sprite(LPDIRECTDRAWSURFACE, int x, int y); LPDIRECTDRAWSURFACE GetSurf() { return surf; } operator LPDIRECTDRAWSURFACE() const { return surf; } int GetX() { return x; } int GetY() { return y; } int GetCenterX() { return x+w/2; } int GetCenterY() { return y+h/2; } void SetXY(int xx, int yy) { x=xx; y=yy; } void SetXYrel(int xx,int yy) { x+=xx; y+=yy; } CRect GetRect(); virtual void Update(); void Hit(Sprite*); void CalcVector(); private: LPDIRECTDRAWSURFACE surf; int x, y; int w, h; int xinc, yinc;

BOOL collide; struct CollideInfo { int x, y; } collideinfo; };


Конструктор класса Sprite

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

Две следующие функции делают одно и то же, но имеют разный синтаксис. Функция GetSurf() и оператор-функция operator LPDIRCETDRAWSURFACE() возвращают указатель на поверхность DirectDraw, которая используется данным спрайтом.


Мы уже видели, как GetSurf() используется функцией SpritesCollidePixel(). Перегруженный оператор LPDIRECTDRAWSURFACE() создан для удобства, благодаря ему объекты Sprite

можно использовать вместо указателей на поверхности DirectDraw. Как вы увидите позднее, этот перегруженный оператор используется в программе Bumper.

Функции GetX(), GetY(), GetCenterX(), GetCenterY(), SetXY(), SetXYRel() и GetRect() предназначены для работы с положением спрайта. Мы уже видели, как функция GetRect() применяется на практике. В программе Bumper функции GetCenterX() и GetCenterY() используются для определения центральной точки спрайта, по которой определяется новое направление движения после столкновения.

Функция CalcVector()

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

Две последние функции, Hit() и Update(), уже упоминались выше. Они обеспечивают подтверждение и реакцию на столкновения.

В закрытой (private) секции объявляются переменные класса Sprite. Первая из них, surf, — указатель на интерфейс DirectDrawSurface, используемый для работы с поверхностью данного объекта Sprite. В переменных x, y, w и h хранятся положение и размеры поверхности. Переменные xinc и yinc

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

В самом конце объявляются переменные collide и collideinfo. При обнаружении столкновения логической переменной collide присваивается значение TRUE, во всех остальных случаях она равна FALSE. Структура collideinfo

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

Сейчас мы подробно рассмотрим все функции класса Sprite. Конструктор класса выглядит так:

Sprite::Sprite(LPDIRECTDRAWSURFACE s, int xx, int yy) { DDSURFACEDESC desc; ZeroMemory( &desc, sizeof(desc) ); desc.dwSize=sizeof(desc); desc.dwFlags=DDSD_WIDTH | DDSD_HEIGHT; s->GetSurfaceDesc( &desc ); surf=s; x=xx; y=yy; w=desc.dwWidth; h=desc.dwHeight;

collide=FALSE;

CalcVector(); }

<


br>Конструктор получает в качестве аргументов указатель на поверхность DirectDraw и исходное положение спрайта. Сохранить эти значения в переменных класса нетрудно, однако мы еще должны инициализировать переменные ширины и высоты (w и h). Для этого необходимо запросить у поверхности DirectDraw ее размеры. С помощью структуры DDSURFACEDESC и функции GetSurfaceDesc() мы узнаем размеры и присваиваем нужные значения переменным. Переменной collide присваивается значение FALSE (потому что столкновение еще не было обнаружено). Наконец, мы вызываем функцию CalcVector(), которая определяется так:

void Sprite::CalcVector() { xinc=(rand()%7)-3; yinc=(rand()%7)-3; }


Функция CalcVector() инициализирует переменные xinc и yinc с помощью генератора случайных чисел rand(). Полученное от rand()

значение преобразуется так, чтобы оно принадлежало интервалу от –3 до 3. Эти значения будут использоваться для перемещения спрайта при очередном обновлении экрана. Обратите внимание — одна или обе переменные вполне могут быть равны нулю. Если нулю равна только одна переменная, перемещение спрайта ограничивается осью X или Y. Если нулю равны обе переменные, спрайт вообще не двигается.

Функция GetRect() инициализирует объект CRect() данными о положении и размерах спрайта. Эта функция определяется так:

CRect Sprite::GetRect() { CRect r; r.left=x; r.top=y; r.right=x+w; r.bottom=y+h; return r; }


Перейдем к функции Hit(). Напомню, что эта функция вызывается при обнаружении столкновения. Функции Hit() передается один аргумент — указатель на спрайт, с которым произошло столкновение. Она выглядит так:

void Sprite::Hit(Sprite* s) { if (!collide) { collideinfo.x=s->GetCenterX(); collideinfo.y=s->GetCenterY(); collide=TRUE; } }


Функция Hit()

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


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

Функция Sprite::Update() выполняет две задачи: обновляет положение спрайта и, в случае столкновения, изменяет переменные, определяющие направление его перемещения (xinc и yinc). Функция Update() приведена в листинге 9.2.

Листинг 9.2. Функция Sprite::Update()

void Sprite::Update() { if (collide) { int centerx=GetCenterX(); int centery=GetCenterY(); int xvect=collideinfo.x-centerx; int yvect=collideinfo.y-centery; if ((xinc>0 && xvect>0) || (xinc<0 && xvect<0)) xinc=-xinc; if ((yinc>0 && yvect>0) || (yinc<0 && yvect<0)) yinc=-yinc; collide=FALSE; }

x+=xinc; y+=yinc;

if (x>640-w/2) { xinc=-xinc; x=640-w/2; } if (x<-(w/2)) { xinc=-xinc; x=-(w/2); } if (y>480-h/2) { yinc=-yinc; y=480-h/2; } if (y<-(h/2)) { yinc=-yinc; y=-(h/2); } }


Сначала Update() проверяет состояние логической переменной collide. Если переменная равна TRUE, мы получаем данные о положении двух спрайтов (текущего и столкнувшегося с ним) и используем их для вычисления новой траектории текущего спрайта. При этом используется схема, очень далекая от настоящей физической модели — при столкновении каждый спрайт отлетает в направлении, противоположном направлению удара.

Затем переменные x и y обновляются с учетом значений xinc и yinc. Новое положение спрайта проверяется и при необходимости корректируется. Корректировка происходит, когда спрайт более чем наполовину уходит за край экрана.

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