::Главная страница :: С++/Си :: Статьи Часть 2

Основы программирования игр с использованием DirectDraw
(перевод статьи Doug Klopfenstein "Basics of DirectDraw Game Programming") (Часть 2)

Материал взят с сайта "Мир программирования"

Использование интерфейсов IDirectDraw2 и IDirectDrawSurface3

Если вы прочитаете статью до конца, вы заметите, что все примеры DDEX используют старые версии интерфейсов IDirectDraw и IDirectDrawSurface. Это все из-за того, что примеры DirectX 5 SDK не были обновлены и не используют интерфейсы IDirectDraw2 и IDirectDrawSurface3. Однако вы всегда можете использовать самые поздние версии этих интерфейсов. Как IDirectDraw2, так и IDirectDrawSurface3 могут быть получены с помощью метода QueryInterface оригинального инетрфейса.

Следующий код показывает, как получить интерфейс IDirectDraw2:

//Создаем интерфейс IDirectDraw2.
LPDIRECTDRAW  lpDD;
LPDIRECTDRAW2 lpDD2;  

ddrval = DirectDrawCreate( NULL, &lpDD, NULL );

if( ddrval != DD_OK )    return;  

ddrval = lpDD->SetCooperativeLevel( hwnd, DDSCL_NORMAL );

if( ddrval != DD_OK )    return;  

ddrval = lpDD->QueryInterface( IID_IDirectDraw2, (LPVOID *)&lpDD2);

if( ddrval != DD_OK )    return;

Следующий кусок кода показывает, как получить интерфейс IDirectDrawSurface3:

LPDIRECTDRAWSURFACE  lpSurf;
LPDIRECTDRAWSURFACE3 lpSurf3;

//Создаем поверхность.
memset( &ddsd, 0, sizeof(ddsd) );
ddsd.dwSize = sizeof( ddsd );
ddsd.dwFlags = DDSD_CAPS | DDSD_WIDTH | DDSD_HEIGHT;
ddsd.ddsCaps.dwCaps = DDSCAPS_OFFSCREENPLAIN |        DDSCAPS_SYSTEMMEMORY;
ddsd.dwWidth = 10;
ddsd.dwHeight = 10;  
ddrval = lpDD2->CreateSurface( &ddsd, &lpSurf, NULL );
if( ddrval != DD_OK )    return;

ddrval = lpSurf->QueryInterface( IID_IDirectDrawSurface3,        (LPVOID *)&lpSurf3);
if( ddrval != DD_OK )    return;

Из-за того, что возможны значительные различия между разными версиями интерфейсов, желательно в своих приложениях не смешивать их использование (например, не использовать одновременно IDirectDrawSurface и IDirectDrawSurface3). Результатом смешивания могут быть неприятные и не всегда ожидаемые последствия.

Установка видеорежима

Следующий шаг работы с DirectDraw - установка видеорежима. Установка видеорежима с использованием DirectDraw состоит из двух шагов. Первое - вызов метода IDirectDraw::SetCooperativeLevel для установки режима низкоуровнего доступа. Как только это требование выполнено, используйте метод IDirectDraw::SetDisplayMode для выбора разрешения экрана.

Работа приложения

Перед тем, как изменить разрешение экрана, как минимум нужно установить флаги DDSCL_EXCLUSIVE и DDSCL_FULLSCREEN параметра dwFlags метода IDirectDraw::SetCooperativeLevel. Это даст вашему приложению эксклюзивный контроль над экраном; в этот момент никакое другое приложение не сможет получить доступ к устройству вывода. Дополнительно флаг DDSCL_FULLSCREEN переводит приложение в полноэкранный режим. Хотя рабочий стол по-прежнему доступен, в чем можно убедится, запустив DDEX1 и нажав клавиши Alt и Tab, ваше приложение занимает весь рабочий стол, и только ваше приложение может отображать что-либо на экране.

Следующий код демонстрирует использование IDirectDraw::SetCooperativeLevel:

HRESULT      ddrval;
LPDIRECTDRAW   lpDD;      // Уже создано методом DirectDrawCreate
ddrval = lpDD->SetCooperativeLevel( hwnd, DDSCL_EXCLUSIVE |           DDSCL_FULLSCREEN );
if( ddrval == DD_OK )
{
        // уже в эксклюзивном режиме.
}
else
{
       // Неудачно.
       // Однако приложение по-прежнему работает.
}

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

Единственное требование IDirectDraw::SetCooperativeLevel - передать функции хэндл окна (HWND) чтобы дать возможность Windows определить, когда ваше приложение завершится. К примеру, если произойдет GPF и GDI переключится на буфер, конечный пользователь не вернется к экрану Windows. Для предотвращения этого, DirectDraw создает процесс, выполняемый в фоновом режиме, который перехватывает сообщения, адресованные этому окну, и использует эти сообщения для определения момента завершения программы. Это налагает некоторые ограничения. Во-первых, вам нужно указать хэндл окна, перехватывающего сообщения. То есть если вы создаете окно, то вы должны быть уверены, что указываете в качестве параметра активное окно. Иначе будут вещи, которые не будут работать, включая возможность GDI определить момент завершения вашего приложения, или возможность переключения по Alt+Tab.

Изменение видеорежимов.

Теперь можно устанавливать видеорежимы, используя метод IDirectDraw::SetDisplayMode. Следующий код показывает, как установить режим 640X480X8 bpp:

HRESULT      ddrval;
LPDIRECTDRAW   lpDD;      // Уже создано  

ddrval = lpDD->SetDisplayMode( 640, 480, 8 );
if( ddrval == DD_OK )
{
   // Режим изменен
}
else
{
   // Режим не может быть изменен.
   // Режим или не поддерживается
   // или кто-то еще находится в эксклюзивном режиме.
}

Когда вы устанавливает какой-нибудь режим, вы должны включить в ваше приложение возможность отката на видеорежим, поддерживаемый большинством видеоадаптеров. Например, ваше приложение должно поддерживать режим 640X480X8 bpp как один из стандартных режимов. (IDirectDraw::SetDisplayMode возвращает код ошибки DDERR_INVALIDMODE если видеоадаптер не поддерживает устанавливаемый режим. В этом случае можно использовать метод IDirectDraw::EnumDisplayMode для определения возможностей оборудования и установить тот режим, который поддерживается)

Создание флиппинг-поверхностей

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

Определение требований поверхности

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

// Создаем главную поверхность с бэк-буфером
ddsd.dwSize = sizeof( ddsd );
ddsd.dwFlags = DDSD_CAPS | DDSD_BACKBUFFERCOUNT;
ddsd.ddsCaps.dwCaps = DDSCAPS_PRIMARYSURFACE | DDSCAPS_FLIP | DDSCAPS_COMPLEX; ddsd.dwBackBufferCount = 1;

В этом примере dwSize содержит размер структуры DDSURFACEDESC. Если этого не сделать, вызовы методов DirectDraw будут возвращать ошибку. (Вообще-то dwSize нужен для будущих расширений струтктуры DDSURFACEDESC)

Поле dwFlags определяет, какие поля структуры DDSURFACEDESC будут заполнены допустимой информацией. Для примера DDEX1 мы установим dwFlags, чтобы использовать структуру DDSCAPS (DDSD_CAPS) и создать бэк-буфер (DDSD_BACKBUFFERCOUNT).

Поле dwCaps определяет флаги, которые будут использованы в структуре DDSCAPS. В нашем случае мы указываем основную поверхность (DDSCAPS_PRIMARYSURFACE), флиппинг-поверхность (DDSCAPS_FLIP), и комплексную поверхность (DDSCAPS_COMPLEX). Комплексная поверхность - поверхность, которя может содержать несколько существующих поверхностей.

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

Память, занятая под поверхность может быть как видео, так и системной. DirectDraw использует системную память, если видеопамяти недостаточно (например, если вы указываете более 1 бэк-буфера на видеокарте с 1 MB памяти). Вы можете указать, какую именно память использовать установкой поля dwCaps структуры DDSCAPS в DDSCAPS_SYSTEMMEMORY или DDSCAPS_VIDEOMEMORY. (Если вы укажете DDSCAPS_VIDEOMEMORY, а этой памяти не хватит для создания поверхности, IDirectDraw::CreateSurface вернет код ошибки DDERR_OUTOFVIDEOMEMORY)

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

Как только структура DDSURFACEDESC заполнена, можно использоватьее и указатель lpDD, указатель на объект DirectDraw созданный функцией DirectDrawCreate, для вызова метода IDirectDraw::CreateSurface как показано в следующем примере:

ddrval = lpDD->CreateSurface( &ddsd, &lpDDSPrimary, NULL );
if( ddrval == DD_OK )
{
   // lpDDSPrimary указывает на созданную поверхность.
}
else
{
   // Поверхность не создана
   return FALSE;
}

Параметр lpDDSPrimary теперь указывает на основную поверхность, созданную IDirectDraw::CreateSurface.

Когда создана основная поверхность вы можете использовать метод IDirectDrawSurface::GetAttachedSurface для определения указателя на бэк-буфер:

ddscaps.dwCaps = DDSCAPS_BACKBUFFER;
ddrval = lpDDSPrimary->GetAttachedSurface( &ddcaps, &lpDDSBack );
if( ddrval == DD_OK )
{
   // lpDDSBack указывает на бэк-буфер.
}
else
{
   return FALSE;
}

Так как мы указали флаг DDSCAPS_BACKBUFFER, то если вызов метода IDirectDrawSurface::GetAttachedSurface был удачен, параметр lpDDSBack будет указывать на бэк-буфер.

Рендеринг поверхности

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

if (lpDDSPrimary->GetDC(&hdc) == DD_OK)
{
   SetBkColor( hdc, RGB( 0, 0, 255 ) );
   SetTextColor( hdc, RGB( 255, 255, 0 ) );
   TextOut( hdc, 0, 0, szFrontMsg, lstrlen(szFrontMsg) );
   lpDDSPrimary->ReleaseDC(hdc);
}
if (lpDDSBack->GetDC(&hdc) == DD_OK)
{
   SetBkColor( hdc, RGB( 0, 0, 255 ) );
   SetTextColor( hdc, RGB( 255, 255, 0 ) );
   TextOut( hdc, 0, 0, szBackMsg, lstrlen(szBackMsg) );
   lpDDSBack->ReleaseDC(hdc);
}

Этот пример использует метод IDirectDrawSurface::GetDC для получения хэндла контекста устройства и блокирования поверхности. Если вы не пользуетесь функциями Windows, которым нужен хэндл контекста устройства, то вы должны использовать методы IDirectDrawSurface::Lock и IDirectDrawSurface::Unlock для блокирования и разблокирования бэк-буфера.

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

Когда поверхность заблокирована, приложение использует стандартные функции Windows GDI SetBkColor для установки цвета фона, SetTextColor для выбора цвета текста, помещаемого на фон, и затем TextOut для отображения текста и фона на поверхности.

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

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

Замечание  После разблокирования с помощью IDirectDrawSurface::Unlock нельзя использовать указатель на память поверхности. Сначала нужно выполнить IDirectDrawSurface::Lock для получения правильного указателя.

Запись и отображение поверхности

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

Запись на поверхность

Первую половину WM_TIMER происходит запсиь сообщения в бэк-буфер. Большинство алгоритмов, используемых при этом, описаны в секции Rendering to the Surfaces, но давайте повторимся. Так это выглядит в DDEX1:

case WM_TIMER:   
// Отображаем поверхность
   if( bActive )
   {
       if (lpDDSBack->GetDC(&hdc) == DD_OK)
       {
           SetBkColor( hdc, RGB( 0, 0, 255 ) );
           SetTextColor( hdc, RGB( 255, 255, 0 ) );
           if( phase )
           {
               TextOut( hdc, 0, 0, szFrontMsg, lstrlen(szFrontMsg) );
               phase = 0;
           }
           else
           {
               TextOut( hdc, 0, 0, szBackMsg, lstrlen(szBackMsg) );
               phase = 1;
           }
           lpDDSBack->ReleaseDC(hdc);
       }
    }

В строке GetDC происходит подготовка к записи - буфер блокируется. Функции SetBkColor и SetTextColor устанавливают цвет фона и текста.

Затем определяется, какое сообщение пишется. Если переменная "phase" равняется 1, пишется сообщение основной поверхности (szFrontMsg) и "phase" устанавливается в 0. Если "phase" равняется 0, пишется сообщение бэк-буфера (szBackMsg) и "phase" устанавливается в 1. Однако заметим, что сообщение в обоих случаях пишется в бэк-буфер.

После записи сообщения в бэк-буфер он разблокируется методом IDirectDrawSurface::ReleaseDC.

Отображение (флиппинг) поверхности

После разблокиования поверхности можно использовать метод IDirectDrawSurface::Flip для отображения бэк-буфера на основную поверхность. Следующий кусок кода демонстрирует, как это делается в DDEX1:

        while( 1 )
       {
           HRESULT ddrval;

           ddrval = lpDDSPrimary->Flip( NULL, 0 );
           if( ddrval == DD_OK )
           {
               break;
           }
           if( ddrval == DDERR_SURFACELOST )
           {
               ddrval = lpDDSPrimary->Restore();
               if( ddrval != DD_OK )
               {
                   break;
               }
           }
           if( ddrval != DDERR_WASSTILLDRAWING )
           {
               break;
           }
       }

В этом примере lpDDSPrimaryопределяет основную поверхность и связанный с нею бэк-буфер. Когда вызывается IDirectDrawSurface::Flip фронт- и бэк-поверхности меняются местами (в смысле, меняются местами указатели; данные остаются неизменные). Если такое переключение прошло успешно, возвращается DD_OK, и приложение выходит из цикла.

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

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

Освобождение памяти, занимаемой объектом DirectDraw

Когда вы нажимаете клавишу F12, приложение DDEX1 приложение отрабатывает сообщение WM_DESTROY перед выходом в систему. При этом вызывается функция finiObjects, которая содержит вызовы IUnknown Release, как показано ниже:

static void finiObjects( void )
{
   if( lpDD != NULL )
   {
       if( lpDDSPrimary != NULL )
       {
           lpDDSPrimary->Release();
           lpDDSPrimary = NULL;
       }
       lpDD->Release();
       lpDD = NULL;
   }
} /* finiObjects */

Здесь все понятно. Приложение проверяет, не равны ли укзатели на объекты DirectDraw (lpDD) и DirectDrawSurface (lpDDSPrimary) NULL, которые, конечно, NULL не равны. Затем DDEX1 вызывает метод IDirectDrawSurface::Release для уменьшения числа ссылок на объект DirectDrawSurface на 1. Когда это число станет равным 0, объект DirectDrawSurface уничтожается. Указатель на DirectDrawSurface обнуляется для исключения ошибочного использования. Приложение вызывает метод IDirectDraw::Release уменьшает число ссылок на объект DirectDraw до 0, тем самым его уничтожая. Затем указатель также обнуляется.

Тематические ссылки
Ваша ссылка Ваша ссылка

Обмен кнопками, ведение статистики, реклама.