DOSUG CZ– розовая кнопка на сайте!
Logo

Введение в DirectDraw и блиттинг поверхностей

BasicDD1.jpg

WinMain и цикл обработки сообщений

Начнем написание нашей программы, выбрав "Win32 Application" в диалоге создания нового проекта среды Visual C++. В первом окне мастера выберем "Simple Win32 Application" и позволим Visual C++ создать функцию WinMain. Код, сгенерированный мастером, выглядит так:

#include "stdafx.h"

int APIENTRY WinMain(HINSTANCE hInstance,
                     HINSTANCE hPrevInstance,
                     LPSTR     lpCmdLine,
                     int       nCmdShow)
{
    // TODO: Place code here.

    return 0;
}

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

HWND InitWindow(int iCmdShow)
{

   HWND      hWnd;
   WNDCLASS  wc;

   wc.style = CS_HREDRAW | CS_VREDRAW;
   wc.lpfnWndProc = WndProc;
   wc.cbClsExtra = 0;
   wc.cbWndExtra = 0;
   wc.hInstance = g_hInst;
   wc.hIcon = LoadIcon(g_hInst, IDI_APPLICATION);
   wc.hCursor = LoadCursor(NULL, IDC_ARROW);
   wc.hbrBackground = (HBRUSH )GetStockObject(BLACK_BRUSH);
   wc.lpszMenuName = TEXT("");
   wc.lpszClassName = TEXT("Basic DD");
   RegisterClass(&wc);

   hWnd = CreateWindowEx(
      WS_EX_TOPMOST,
      TEXT("Basic DD"),
      TEXT("Basic DD"),
      WS_POPUP,
      0,
      0,
      GetSystemMetrics(SM_CXSCREEN),
      GetSystemMetrics(SM_CYSCREEN),
      NULL,
      NULL,
      g_hInst,
      NULL);

   ShowWindow(hWnd, iCmdShow);
   UpdateWindow(hWnd);
   SetFocus(hWnd);

   return hWnd;

}

Первое, что делает эта функция, - это регистрирует класс окна в среде Windows. После заполнения структуры WNDCLASS информация о регистрируемом классе передается в функцию RegisterClass. Заметим, что впоследствии нам понадобится хэндл приложения, поэтому сохраним его в глобальной переменной g_hInst. Еще одна переменная будет содержать хэндл основного окна программы (которое мы собственно и создаем). Обе переменные объявлены чуть выше определения функции WinMain:

HWND        g_hMainWnd;
HINSTANCE    g_hInst;

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

    g_hInst = hInstance;
    g_hMainWnd = InitWindow(nCmdShow);

    if(!g_hMainWnd)
        return -1;

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

LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM 
lParam)
{
   switch (message)
   {
   case WM_DESTROY:
      PostQuitMessage(0);
      return 0;

   } // switch

   return DefWindowProc(hWnd, message, wParam, lParam);
} //

Итак, ваше приложение почти готово к запуску, единственное, что еще нужно сделать - реализовать цикл обработки сообщений. Для того, чтобы приложение смогло "отловить" сообщения, нам нужно вызвать функцию, которая будет обрабатывать пришедшие сообщения. Если мы получили одно из таких сообщений, нам нужно обработать его в функции WndProc. Если никаких сообщений не поступало, мы можем использовать "свободное время" для выполнения каких-либо фоновых вычислений или для задач DirectX. Этот процесс называется idle processing - обработка во время простоя. Цикл обработки сообщений нужно вставить прямо за инициализацией наших глобальных переменных.

while( TRUE )
{
    MSG msg;

    if( PeekMessage( &msg, NULL, 0, 0, PM_REMOVE ) )
    {
        // проверяем, не поступило ли сообщение о выходе из программы
        if( msg.message == WM_QUIT )
            break;

        TranslateMessage( &msg );
        DispatchMessage( &msg );
    }
    else
    {
        ProcessIdle();
    }
}

В цикле проверяется, есть ли еще в очереди сообщения, которые нужно обработать. Это выполняется вызовом функции PeekMessage. Если функция возвращает истину, мы вызываем функции TranslateMessage и DispatchMessage, что вызывает обработку очередного сообщения. Если сообщений в очереди нет, вызывается функция ProcessIdle. Мы напишем эту функцию так, что она будет обновлять картинку на экране. Вот как определяется эта функция:

void ProcessIdle()
{

}

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

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

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

Итак, вся графика хранится в памяти в структурах, называемых поверхностями. Фактически, в приложениях DirectDraw, область, которую мы видим на экране, тоже рассматривается как поверхность, называемая видимой поверхностью или фронт-буфером (frontbuffer). Вместе с видимой поверхностью ассоциируется поверхность, называемая невидимой поверхностью или бэк-буфером (backbuffer). Эта поверхность содержит информацию, которая будет показана пользователю в следующем кадре. Предположим, пользователь видит первый объект на экране в позиции (10,10) , а второй объект - в позиции (100,100). В процессе перемещения первый объет должен оказаться в позиции (12,10), второй объект - в позиции (102, 10). Если мы будем формировать картинку прямо на видимой поверхности, возникнут проблемы с синхронизацией (т.е. пользователь увидит, что сначала переместился первый объект, а затем - второй, но реально они должны перемещаться одновременно, т.е. синхронно). Для решения этой проблемы мы создаем каждый следующий кадр на невидимой поверхности. После совершения всех передвижений в бэк-буфере мы перемещаем информацию с невидимой поверхности на видимую. Этот процесс называется флиппингом и он очень напоминает процесс создания мультипликационных фильмов (когда мы используем множество картинок, нарисованных на бумаге).

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

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

#include <ddraw.h>

Также проекту нужно "подсказать", какие библиотеки нужны для работы DirectDraw. Идем в меню Project, подменю Settings. Выберите закладку "Link" и добавьте следующие библиотеки в поле "Object/Library Modules" (конечно, если их там уже нет)

kernel32.lib user32.lib ddraw.lib dxguid.lib gdi32.lib

Теперь нам нужно создать в программе новую функцию. Эта функция будет называться InitDirectDraw и основной ее целью будет выполнение подготовительных операций - создание объекта DirectDraw и создание двух поверхностей (видимой и невидимой).

int InitDirectDraw()
{
   DDSURFACEDESC2 ddsd;
   DDSCAPS2       ddscaps;
   HRESULT          hRet;

   // Создаем объект DirectDraw.
   hRet = DirectDrawCreateEx(NULL, (VOID**)&g_pDD, IID_IDirectDraw7, 
                             NULL);
   if( hRet != DD_OK )
       return -1;

   // Устанавливаем эксклюзивный режим.
   hRet = g_pDD->SetCooperativeLevel(g_hMainWnd, 
                                     DDSCL_EXCLUSIVE | DDSCL_FULLSCREEN);
   if( hRet != DD_OK )
       return -2;

   // Устанавливаем разрешение 640x480x16.
   hRet = g_pDD->SetDisplayMode(640, 480, 16, 0, 0);
   if( hRet != DD_OK )
       return -3;

   // Подготовка к созданию основной поверхности -
   // очистка памяти и заполнение структуры DDSURFACEDESC2.
   ZeroMemory(&ddsd, sizeof(ddsd));
   ddsd.dwSize = sizeof(ddsd);
   ddsd.dwFlags = DDSD_CAPS | DDSD_BACKBUFFERCOUNT;
   ddsd.ddsCaps.dwCaps = DDSCAPS_PRIMARYSURFACE | DDSCAPS_FLIP |
      DDSCAPS_COMPLEX;
   ddsd.dwBackBufferCount = 1;

   // Создаем основную поверхность.
   hRet = g_pDD->CreateSurface(&ddsd, &g_pDDSFront, NULL);
   if( hRet != DD_OK )
       return -1;

   // Получаем указатель на невидимую поверхность.
   ZeroMemory(&ddscaps, sizeof(ddscaps));
   ddscaps.dwCaps = DDSCAPS_BACKBUFFER;
   hRet = g_pDDSFront->GetAttachedSurface(&ddscaps, &g_pDDSBack);
   if( hRet != DD_OK )
       return -1;

   return 0;

} 

Вы, наверное, заметили, что в коде используются несколько необъявленных ранее переменных с префиксом "g_" (глобальная переменная). Так как указатели на видимую и невидимую поверхность будут использоваться во всей нашей программе, поместим их в глобальные переменные. Еще одна очень важная переменная - в ней мы будем хранить объект DirectDraw - g_pDD. Этот объект используется при создании всех остальных объектов, используемых в DirectDraw. Итак, в самом начале нашего кода добавим еще несколько строчек:

LPDIRECTDRAW7        g_pDD = NULL;        // объект DirectDraw
LPDIRECTDRAWSURFACE7 g_pDDSFront = NULL;  // видимая поверхность DirectDraw - фронтбуфер
LPDIRECTDRAWSURFACE7 g_pDDSBack = NULL;   // невидимая поверхность DirectDraw - бэкбуфер

Вернемся к функции InitDirectDraw. Первое, что в ней делается - создается объект DirectDraw. Для этого используется функция DirectDrawCreate, которая объявлена в файле ddraw.h. Наиболее важны два параметра этой функции - второй и третий. Второй параметр - это ссылка на переменную, в которой мы будем хранить указатель на объект (в нашем случае это переменная g_pDD). В третьем параметре передается идентификатор версии объекта DirectDraw. Это позволяет работать с ранними версиями DirectDraw после инсталляции новых версий SDK. Например, можно работать с объектами DirectX 7, используя DX SDK 8.1.

Как вы, наверное, заметили, результат функции сравнивается с константой DD_OK, которая означает, что функция выполнилась успешно (это справедливо для всех функций DirectDraw). Важно проверять каждое значение, возвращаемое функциями DirectDraw. Если значение не равно DD_OK, функция InitDirectDraw возвращает -1. В этом случае можно предположить, что на компьютере пользователя не установлена нужная версия DirectX и выдать предупреждающее сообщение (позднее будет продемонстрировано, как это сделать).

Второй вызов функции - SetCooperativeLevel. Эта функция используется для того, чтобы "сообщить" DirectX, каким образом мы собираемся работать с экраном - в полноэкранном режиме или оконном и т.п. Все доступные опции этой функции приведены в документации к DirectX. Опять же результат функции проверяется на корректность.

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

После процедур инициализации нам нужно создать две поверхности, которые будут использованы для вывода графики на экран. Сначала инициализируется видимая поверхность. Для создания поверхности в DirectDraw нужно правильно инициализировать поля структуры DDSURFACEDESC2. Очень важно сначала очистить память, занимаемую структурой, функцией ZeroMemory или memset (иначе в некоторых случаях могут возникнуть проблемы). Создавая видимую поверхность, параметру dwflags нужно присвоить значение DDSD_BACKBUFFERCOUNT, указывающее, что поверхность видимая и ей соответствует невидимая поверхность. Параметр ddsCaps.dwCaps нужно инициализировать значением DDSCAPS_PRIMARYSURFACE. Т.к. мы будем осуществлять флиппинг поверхностей, нужно указать параметры DDSCAPS_FLIP и DDSCAPS_COMPLEX.

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

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

Итак, мы завершили написание функции инициализации DirectDraw, теперь ее нужно вызвать из основной программы. Вот как это выглядит:

if(InitDirectDraw() < 0)
{
    CleanUp();
    MessageBox(g_hMainWnd, 
               "Could start DirectX engine in your computer." 
               "Make sure you have at least version 7 of "
               "DirectX installed.", 
           "Error", MB_OK | MB_ICONEXCLAMATION);
    return 0;
}

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

Здесь вызывается еще одна функция - CleanUp - которую мы еще не описали. Эта функция должна удалять все созданные объекты DirectX. Все объекты удаляются вызовом метода Release. Вот реализация этой функции:

void CleanUp()
{
    if(g_pDDSBack)
        g_pDDSBack->Release();

    if(g_pDDSFront)
        g_pDDSFront->Release();

    if(g_pDD)
        g_pDD->Release();
}

Прежде, чем мы откомпилируем и запустим приложение, вставьте следующий фрагмент в функцию WndProc, в оператор switch обработки сообщений:

   case WM_KEYDOWN:
       if(wParam == VK_ESCAPE)
       {
            PostQuitMessage(0);
            return 0;
       }
       break;

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

Блиттинг графики

Теперь мы займемся рисованием на невидимой поверхности и флиппингом поверхностей, что в конечном итоге создаст на экране анимированную картинку. Для создания анимации мы будем использовать битмап с несколькими изображениями машины. Для создания спрайта в DirectDraw нам нужно сохранить битмап на отдельной поверхности (которую мы будем называть тайлом или закадровой поверхностью), которую мы затем будем копировать на невидимую поверхность. Для удобства манипулирования тайловыми поверхностями мы создадим класс cSurface. Щелкните правой кнопкой мыши в окне ClassView среды Visual C++ и выберите пункт Create New Class (или выберите пункт меню "Insert" -> "New Class..."). Тип класса оставьте неизменным - Generic Class, а в качестве имени укажите cSurface.

Начнем описание методов нашего класса. Основная переменная будет иметь тип LPDIRECTDRAWSURFACE7, она будет служить для хранения объекта DirectDraw Surface (поверхность DirectDraw). Также мы должны хранить длину и ширину поверхности. Еще одно поле класса - m_ColorKey - подробнее о нем мы поговорим позднее. Вот как выглядит описание переменных создаваемого класса:

protected:
    COLORREF m_ColorKey;
    UINT m_Height;
    UINT m_Width;
    LPDIRECTDRAWSURFACE7 m_pSurface;

Первая функция, которая будет создана в классе - функция Create. Эта функция будет создавать объект типа поверхности DirectX. Вот реализация функции Create:

BOOL cSurface::Create(LPDIRECTDRAW7 hDD, int nWidth, int nHeight, 
                      COLORREF dwColorKey)
{
    DDSURFACEDESC2    ddsd;
    HRESULT        hRet;
    DDCOLORKEY          ddck;


    ZeroMemory( &ddsd, sizeof( ddsd ) );

    ddsd.dwSize = sizeof( ddsd );
    ddsd.dwFlags = DDSD_CAPS | DDSD_WIDTH | DDSD_HEIGHT;
    ddsd.ddsCaps.dwCaps = DDSCAPS_OFFSCREENPLAIN | DDSCAPS_VIDEOMEMORY;
    
    ddsd.dwWidth  = nWidth;
    ddsd.dwHeight = nHeight;

    hRet = hDD->CreateSurface(&ddsd, &m_pSurface, NULL );
    if( hRet != DD_OK )
    {
        
       if(hRet == DDERR_OUTOFVIDEOMEMORY)
       {
          ddsd.ddsCaps.dwCaps = DDSCAPS_OFFSCREENPLAIN |
                  DDSCAPS_SYSTEMMEMORY;
    
          hRet = hDD->CreateSurface(&ddsd, &m_pSurface, NULL );
       }

       if( hRet != DD_OK )
       {
          return FALSE;
       }
    }


    if((int)dwColorKey != -1)
    {
       ddck.dwColorSpaceLowValue = dwColorKey;
       ddck.dwColorSpaceHighValue = 0;
       m_pSurface->SetColorKey(DDCKEY_SRCBLT, &ddck);
    }

    m_ColorKey = dwColorKey;
    m_Width  = nWidth;
    m_Height = nHeight;

    return TRUE;
}

Как вы наверное заметили, процесс создания тайловой поверхности мало чем отличается от процесса создания видимой поверхности. Вся разница заключается в заполнении полей структуры DDSURFACEDESC2. Параметр dwFlags указывает, что мы будем заполнять поля dwCaps, dwWidth и dwHeight и что их нужно учитывать при создании поверхности. Параметр dwCaps, равный DSCAPS_OFFSCREENPLAIN, указывает, что поверхность закадровая, т.е. невидима. Указывая флаг DDSCAPS_VIDEOMEMORY, мы даем понять, что хотим создать поверхность в видеопамяти.

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

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

Теперь мы займемся созданием еще одной функции, загружающей битмап на поверхность DirectX. Для этого воспользуемся некоторыми функциями GDI. Так как нам нужно загрузить всего одну картинку, функция не будет влиять на общую производительность приложения. Функция LoadBitmap выглядит следующим образом:

BOOL cSurface::LoadBitmap(HINSTANCE hInst, UINT nRes, int nX, int nY, 
                          int nWidth, int nHeight)
{
    HDC                     hdcImage;
    HDC                     hdc;
    BITMAP                  bm;
    DDSURFACEDESC2          ddsd;
    HRESULT                 hr;

    HBITMAP    hbm;

    hbm = (HBITMAP) LoadImage(hInst, MAKEINTRESOURCE(nRes), 
                              IMAGE_BITMAP, nWidth, nHeight, 0L);

    if (hbm == NULL || m_pSurface == NULL)
        return FALSE;

    // Убедимся, что поверхность не "потеряна".
    m_pSurface->Restore();

    hdcImage = CreateCompatibleDC(NULL);
    if (!hdcImage)
        return FALSE;

    SelectObject(hdcImage, hbm);

    // Получаем размер картинки
    GetObject(hbm, sizeof(bm), &bm);

    if(nWidth == 0)
    nWidth = bm.bmWidth;
    
    if(nHeight == 0)
    nHeight = bm.bmHeight;
    
    // Определяем размер поверхности.
    ddsd.dwSize = sizeof(ddsd);
    ddsd.dwFlags = DDSD_HEIGHT | DDSD_WIDTH;
    m_pSurface->GetSurfaceDesc(&ddsd);

    if ((hr = m_pSurface->GetDC(&hdc)) == DD_OK)
    {
        StretchBlt(hdc, 0, 0, ddsd.dwWidth, ddsd.dwHeight, hdcImage, 
                   nX, nY, nWidth, nHeight, SRCCOPY);
        m_pSurface->ReleaseDC(hdc);
    }
    DeleteDC(hdcImage);

    m_srcInfo.m_hInstance = hInst;
    m_srcInfo.m_nResource = nRes;
    m_srcInfo.m_iX          = nX;
    m_srcInfo.m_iY          = nY;
    m_srcInfo.m_iWidth    = nWidth;
    m_srcInfo.m_iHeight      = nHeight;
    
    return TRUE;
}

Если вы хотя бы немного работали с функциями GDI, этот фрагмент не покажется вам слишком сложным. Во-первых, нужно вызвать метод Restore переменной m_Surface. Это нужно для восстановления памяти, которая может быть освобождена DirectDraw (если возникает такая ситуация, любое обращение к объекту m_Surface возвращает код ошибки DERR_SURFACELOST). После восстановления памяти мы создаем DC и загружаем картинку из ресурсов. Затем битмап копируется на поверхность с помощью функции StretchBlt. Также мы сохраняем информацию о битмапе в структуре m_srcInfo. Эта структура используется в случае, если поверхность "потеряется", так что мы всегда можем восстановить поверхность и изображение, хранящееся на ней.

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

BOOL cSurface::Draw(LPDIRECTDRAWSURFACE7 lpDest, int iDestX, int iDestY, 
                    int iSrcX, int iSrcY, int nWidth, int nHeight)
{
    RECT    rcRect;
    HRESULT    hRet;

    if(nWidth == 0)
        nWidth = m_Width;

    if(nHeight == 0)
        nHeight = m_Height;

    rcRect.left   = iSrcX;
    rcRect.top    = iSrcY;
    rcRect.right  = nWidth  + iSrcX;
    rcRect.bottom = nHeight + iSrcY;

    while(1)
    {
        if((int)m_ColorKey < 0)
        {
            hRet = lpDest->BltFast(iDestX, iDestY, m_pSurface, 
                                   &rcRect,  DDBLTFAST_NOCOLORKEY);
        }
        else
        {
            hRet = lpDest->BltFast(iDestX, iDestY, m_pSurface, 
                                   &rcRect,  DDBLTFAST_SRCCOLORKEY);
        }

        if(hRet == DD_OK)
            break;

        if(hRet == DDERR_SURFACELOST)
        {
            Restore();
        }
        else
        {
            if(hRet != DDERR_WASSTILLDRAWING)
                return FALSE;
        }
    }

    return TRUE;

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

И, наконец, функция Destroy создана для освобождения ресурсов DirectDraw, занимаемых приложением. В нашем случае мы вызываем метод Release объекта m_Surface.

void cSurface::Destroy()
{

    if(m_pSurface != NULL)
    {
        m_pSurface->Release();
        m_pSurface = NULL;
    }
}

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

Рисование на невидимой поверности с помощью класса cSurface

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

#include "csurface.h"

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

cSurface    g_surfCar;

После этого добавьте в приложение ресурс - картинку из файла bmp_bigcar_green.bmp . Создайте идентификатор (ID) ресурса с именем "IDB_GREENCAR".

Итак, у нас есть экземпляр класса, нужно вызвать методы создания и загрузки картинки. Этот фрагмент кода должен вызываться после вызова функции InitDirectDraw.

g_surfCar.Create(g_pDD, 1500, 280);
g_surfCar.LoadBitmap(g_hInst, IDB_GREENCAR, 0, 0, 1500, 280);

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

void CleanUp()
{
    g_surfCar.Destroy();

    if(g_pDDSBack)
        g_pDDSBack->Release();

    if(g_pDDSFront)
        g_pDDSFront->Release();

    if(g_pDD)
        g_pDD->Release();
}

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

void ProcessIdle()
{
    HRESULT hRet;

    g_surfCar.Draw(g_pDDSBack, 245, 170, 0, 0, 150, 140);

    while( 1 )
    {
        hRet = g_pDDSFront->Flip(NULL, 0 );
        if( hRet == DD_OK )
        {
            break;
        }
        if( hRet == DDERR_SURFACELOST )
        {
            g_pDDSFront->Restore();
        }
        if( hRet != DDERR_WASSTILLDRAWING )
        {
            break;
        }
    }
}

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

void ProcessIdle()
{
    HRESULT hRet;
    static int iX = 0, iY = 0;
    static iLastBlit;

    if(GetTickCount() - iLastBlit < 50)
    {
        return;
    }

    g_surfCar.Draw(g_pDDSBack, 245, 170, iX, iY, 150, 140);

    while( 1 )
    {
        hRet = g_pDDSFront->Flip(NULL, 0 );
        if( hRet == DD_OK )
        {
            break;
        }
        if( hRet == DDERR_SURFACELOST )
        {
            g_pDDSFront->Restore();
        }
        if( hRet != DDERR_WASSTILLDRAWING )
        {
            break;
        }
    }

    iX += 150;
    if(iX >= 1500)
    {
        iX = 0;
        iY += 140;
        if(iY >= 280)
        {
            iY = 0;
        }
    }

    iLastBlit = GetTickCount();
}

Здесь мы создаем 3 статических переменных. Первые используются для изменения позиции копируемой части исходной картинки. Таким образом мы можем создать эффект анимации, последовательно проходя от кадра 1 до 20. Третья переменная, iLastBlit, хранит результат выполнения функции GetTickCount. Этот результат используется для того, чтобы задержать кадр по меньшей мере на 50 миллисекунд на экране, что нужно для плавного перемещения картинки. Можете удалить фрагмент кода и посмотреть, что получится (на хорошем компьютере картинка будет вращаться очень быстро).

Источник:
Перевод:
Тарадым Александр, "Мир программирования"
главная - о проекте - контакты - реклама на сайте
 
LBN100 Elite

SoftStudio.Ru - студия разработки программ
LBN100 Elite