DirectX для начинающих — Sprite

Материал из DRKB


Этот пример демонстрирует вывод изображения средствами DirectDraw. Он построен без использования VCL, но я надеюсь это не помешает вам. Вот файлы проекта:

  • sprite.dpr - отвечает за создание окна и выборку системных сообщений.
  • basedd8.pas - содержит функции для работы с DirectDraw.


basedd8.pas - содержит функции для работы с DirectDraw.
(******************************************************************************
 *                                                                            *
 *  Придумал и написал Кода Виктор, Март 2002                                 *
 *                                                                            *
 *  Файл:       basedd8.pas                                                   *
 *  Содержание: Процедуры и функции для работы с DirectDraw API               *
 *                                                                            *
 ******************************************************************************)
unit basedd8;

interface

uses
  windows,
  directdraw;

// Константы и глобальные переменные
const
  SCREEN_WIDTH  = 640; // Ширина экрана
  SCREEN_HEIGHT = 480; // Высота экрана

// Прототипы функций
function InitDirectDraw(hWnd: HWND; dwWidth, dwHeight, dwBPP: DWORD): Boolean;
procedure ReleaseDirectDraw();
function CopyImage(var lpSurf: IDirectDrawSurface7; hImage: HBITMAP): Boolean;
function LoadFiles(hWnd: HWND): Boolean;
procedure OnDraw(nX, nY: Integer);

implementation

// Константы и глобальные переменные
var
  lpDD7:            IDirectDraw7        = nil;  // Главный объект DirectDraw
  lpPrimarySurface: IDirectDrawSurface7 = nil;  // Первичная поверхность
  lpBackBuffer:     IDirectDrawSurface7 = nil;  // Дополнительный буфер
  lpWallpaper:      IDirectDrawSurface7 = nil;  // Задний фон
  lpSprite:         IDirectDrawSurface7 = nil;  // Спрайт

// Производит инициализацию объектов DirectDraw в программе
function InitDirectDraw(hWnd: HWND; dwWidth, dwHeight, dwBPP: DWORD): Boolean;
var
  ddsd2: TDDSURFACEDESC2;
  ddsc2: TDDSCAPS2;
begin
  Result := FALSE;

  // Создаём главный объект DirectDraw
  if FAILED(DirectDrawCreateEx(nil, lpDD7, IID_IDIRECTDRAW7, nil)) then
    Exit;
  lpDD7._AddRef();

  // Устанавливаем уровень кооперации
  if FAILED(lpDD7.SetCooperativeLevel(hWnd, DDSCL_ALLOWREBOOT or DDSCL_EXCLUSIVE or DDSCL_FULLSCREEN)) then
    Exit;

  // Устанавливаем требуемое разрешение и глубину цвета
  if FAILED(lpDD7.SetDisplayMode(dwWidth, dwHeight, dwBpp, 0, 0)) then
    Exit;

  // Подготавливаем структуру TDDSURFACEDESC2
  ZeroMemory(@ddsd2, SizeOf(TDDSURFACEDESC2));
  ddsd2.dwSize := SizeOf(TDDSURFACEDESC2);

  ddsd2.dwFlags := DDSD_CAPS or DDSD_BACKBUFFERCOUNT;
  ddsd2.ddsCaps.dwCaps := DDSCAPS_PRIMARYSURFACE or DDSCAPS_FLIP or DDSCAPS_COMPLEX;
  ddsd2.dwBackBufferCount := 1;

  // Создаём первичную поверхность (фактически постоянно видимую на экране)
  if FAILED(lpDD7.CreateSurface(ddsd2, lpPrimarySurface, nil)) then
    Exit;
  lpPrimarySurface._AddRef();

  // Подготавливаем структуру TDDSCAPS2
  ZeroMemory(@ddsc2, SizeOf(TDDSCAPS2));
  ddsc2.dwCaps := DDSCAPS_BACKBUFFER; // Указываем, что поверхность будет буфером

  // Подсоединяем к первичной поверхности дополнительный буфер
  if FAILED(lpPrimarySurface.GetAttachedSurface(ddsc2, lpBackBuffer)) then
    Exit;
  lpBackBuffer._AddRef();

  Result := True;
end;

// Удаляет из памяти объекты DirectDraw
procedure ReleaseDirectDraw();
begin
  // Удаляем сперва дополнительный буфер
  if lpBackBuffer <> nil then
  begin
    lpBackBuffer._Release();
    lpBackBuffer := nil;
  end;

  // Теперь можно удалить первичную поверхность
  if lpPrimarySurface <> nil then
  begin
    lpPrimarySurface._Release();
    lpPrimarySurface := nil;
  end;

  // Наконец, последним удаляем главный объект DirectDraw
  if lpDD7 <> nil then
  begin
    lpDD7.RestoreDisplayMode(); // Восстанавливаем прежний видеорежим
    lpDD7._Release();
    lpDD7 := nil;
  end;
end;

// Описание: Создаёт поверхность DirectDraw нужных размеров
function CreateSurface(var lpSurface: IDirectDrawSurface7; nWidth, nHeight: Integer): Boolean;
var
  ddsd2: TDDSURFACEDESC2;
begin
  Result := False;

  // Если поверхность существует, то удаляем
  if lpSurface <> nil then
  begin
    lpSurface._Release;
    lpSurface := nil;
  end;

  // Подготавливаем структуру TDDSURFACEDESC2
  ZeroMemory(@ddsd2, SizeOf(TDDSURFACEDESC2));
  with ddsd2 do
  begin
    dwSize         := SizeOf(TDDSURFACEDESC2);
    dwFlags        := DDSD_CAPS or DDSD_WIDTH or DDSD_HEIGHT;
    ddsCaps.dwCaps := DDSCAPS_OFFSCREENPLAIN;
    dwWidth        := nWidth;
    dwHeight       := nHeight;
  end;

  // Создаём поверхность
  if FAILED(lpDD7.CreateSurface(ddsd2, lpSurface, nil)) then
    Exit;
  lpSurface._AddRef;

  Result := True;
end;

// Копирует изображение на поверхность DirectDraw
function CopyImage(var lpSurf: IDirectDrawSurface7; hImage: HBITMAP): Boolean;
var
  destDC,          // Поверхность-приёмник
  srcDC:  HDC;     // Поверхность-источник
  bm:     BITMAP;  // BITMAP из windows.pas
  hObj:   HGDIOBJ; // Объект GDI
begin
  Result := False;

  // Создаём контекст памяти, совместимый с Рабочим столом
  srcDC := CreateCompatibleDC(0);

  // Если контекст не получен - обрабатываем ошибку
  if srcDC = 0 then Exit;

  // Выбираем в контекст растр
  hObj := SelectObject(srcDC, hImage);
  if (hObj = 0) or (hObj = GDI_ERROR) then
    Exit;

  // Получаем размеры растра, данные заполняют структуру BITMAP
  if GetObject(hImage, SizeOf(bm), @bm) = 0 then
    Exit;

  // Пересоздаём поверхность с размерами растра
  if not CreateSurface(lpSurf, bm.bmWidth, bm.bmHeight) then
    Exit;

  // Создаём GDI-совместимый контекст для поверхности
  if SUCCEEDED(lpSurf.GetDC(destDC)) then
  begin
    // BitBlt-им в контексте (а фактически на поверхности DirectDraw)
    BitBlt(destDC, 0, 0, bm.bmWidth, bm.bmHeight, srcDC, 0, 0, SRCCOPY);
    lpSurf.ReleaseDC(destDC);
  end
  else
    Exit;

  // В конце операции удаляем объекты GDI
  DeleteObject(hObj);
  DeleteDC(srcDC);

  Result := True;
end;

// Загружает растр из bmp-файлов на поверхность DirectDraw
function LoadFiles(hWnd: HWND): Boolean;
var
  hImage: HBITMAP;
begin
   Result := FALSE;

   // Загружаем изображение и получаем его хэндл
   hImage := LoadImage(0, 'wallpaper.bmp', IMAGE_BITMAP, 0, 0,
                       LR_LOADFROMFILE or LR_CREATEDIBSECTION);
   if hImage = 0 then
   begin
     MessageBox(hWnd, 'Не загружен файл wallpaper.bmp!', 'Ошибка!', MB_ICONHAND);
     Exit;
   end;

   // Копируем растр на поверхность DirectDraw
   if not CopyImage(lpWallpaper, hImage) then
     Exit;

   // Теперь для подвижного спрайта (файл картинки я взял из DSK).
   hImage := LoadImage(0, 'sprite.bmp', IMAGE_BITMAP, 0, 0,
                       LR_LOADFROMFILE or LR_CREATEDIBSECTION);
   if hImage = 0 then
   begin
     MessageBox(hWnd, 'Не загружен файл sprite.bmp!', 'Ошибка!', MB_ICONHAND);
     Exit;
   end;

   if not CopyImage(lpSprite, hImage) then
     Exit;

   Result := True;
end;

// Осуществляет вывод на экран фона и спрайта
procedure OnDraw(nX, nY: Integer);
var
  rRect: TRect;
begin
  // Устанавливаем размер прямоугольника под размер поверхности
  SetRect(rRect, 0, 0, SCREEN_WIDTH, SCREEN_HEIGHT);
  // Копируем изображение фона на задний буфер
  lpBackBuffer.BltFast(0, 0, lpWallpaper, @rRect, DDBLTFAST_WAIT);

  // Аналогично для спрайта
  SetRect(rRect, 0, 0, 256, 256);
  lpBackBuffer.BltFast(nX, nY, lpSprite, @rRect, DDBLTFAST_WAIT);

  // Теперь быстро переносим содержимое заднего буфера на первичную поверхность
  lpPrimarySurface.Flip(nil, DDFLIP_WAIT);
end;

end.


sprite.dpr - отвечает за создание окна и выборку системных сообщений.
(******************************************************************************
 *                                                                            *
 *  Придумал и написал Кода Виктор, Март 2002                                 *
 *                                                                            *
 *  Файл:  sprite.dpr                                                    *
 *  Содержание: Пример использования DirectDraw в Delphi                      *
 *                                                                            *
 ******************************************************************************)
program Sprite;

uses
  Windows,
  Messages,
  basedd8 in 'basedd8.pas';

// Константы и глобальные переменные
const
  APP_NAME = 'DDDemo';
  WIN_NAME = 'DDDemo';

var
  wce: WNDCLASSEX;
  hWindow: HWND;
  lpMSG: MSG;
  nPosX, nPosY: integer; // Координаты спрайта

// Описание: Оконная функция
function WndProc(hWnd: HWND; msg: UINT; wParam: WPARAM; lParam: LPARAM): LRESULT; stdcall;
begin
  case msg of
    WM_KEYDOWN:
      case wParam of
        VK_ESCAPE: SendMessage(hWnd, WM_DESTROY, 0, 0);
        VK_LEFT: Dec(nPosX, 5);
        VK_RIGHT: Inc(nPosX, 5);
        VK_UP: Dec(nPosY, 5);
        VK_DOWN: Inc(nPosY, 5);
      end;
     WM_DESTROY:
    begin
      PostQuitMessage(0);
      Exit;
    end;
  end;
  WndProc := DefWindowProc(hWnd, msg, wParam, lParam);
end;

// Описание: Здесь программа получает управление
begin
  with wce do
  begin
    cbSize := SizeOf(WNDCLASSEX);
    style := CS_HREDRAW or CS_VREDRAW;
    lpfnWndProc := @WndProc;
    hCursor := LoadCursor(0, IDC_ARROW);
    hbrBackground := HBRUSH(GetStockObject(BLACK_BRUSH));
    lpszClassName := APP_NAME;
  end;

  RegisterClassEx(wce);

  hWindow := CreateWindowEx(WS_EX_TOPMOST, APP_NAME, WIN_NAME, WS_POPUP,
    0, 0, GetSystemMetrics(SM_CXSCREEN),
    GetSystemMetrics(SM_CYSCREEN), 0, 0,
    SysInit.HInstance, nil);

  ShowWindow(hWindow, CmdShow);
  UpdateWindow(hWindow);

  // Здесь начинается театр :-)
  if (InitDirectDraw(hWindow, SCREEN_WIDTH, SCREEN_HEIGHT, 16)) and (LoadFiles(hWindow)) then
    while True do
    begin
      if PeekMessage(lpMsg, 0, 0, 0, PM_NOREMOVE) then
        if not GetMessage(lpMsg, 0, 0, 0) then
          Break
        else
        begin
          TranslateMessage(lpMsg);
          DispatchMessage(lpMsg);
        end
      else
      begin
        // Проверяем выход спрайта за границы экрана
        if nPosX < 0 then
          nPosX := 0;
        if nPosY < 0 then
          nPosY := 0;
        if nPosX > SCREEN_WIDTH - 256 then
          nPosX := SCREEN_WIDTH - 256;
        if nPosY > SCREEN_HEIGHT - 256 then
          nPosY := SCREEN_HEIGHT - 256;
        // 256 - это ширина и высота спрайта, учитываем их

        // Рисуем на экране с помощью DirectDraw
        OnDraw(nPosX, nPosY);
      end;
    end;

  // Удаляем объекты DirectDraw
  ReleaseDirectDraw();

  DestroyWindow(hWindow);
end.


После регистрации класса окна и его отображения производится инициализация DirectDraw. За это отвечает функция InitDirectDraw(). Рассмотрим её работу подробнее.


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

Параметр iid может принимать только одно значение - IID_IDIRECTDRAW7. Любое другое значение приведёт к ошибке DDERR_INVALPARAMS. Последний параметр в описываемой функции на данный момент не используется и предназначен для будущих версий. Результат, возвращаемый функцией (равно как и любой другой в DirectX), имеет тип HRESULT, т.е. может иметь множество кодов завершения. В DirectDraw успешная работа функции определяется константой DD_OK, а ошибочная - множеством других, зависящих от типа ошибки. Кроме упоминавшейся константы DDERR_INVALPARAMS функция может вернуть DDERR_UNSUPPORTED, сообщив тем самым, что объект DirectDraw не инициализирован. Чтобы избежать мороки с проверкой возвращённого результата, рекомендуется применять макросы (в Delphi практически функции) FAILED() и SUCCEEDED(), определённые в windows.pas. Дело в том, что 31-й бит возвращаемого значения содержит флаг критичности, который указывает, произошла ли ошибка или нет. Если бит равен 0, то ошибки не было, а если он равен 1, то ошибка имела место. Это как раз и проверяют вышеуказанные макросы. Конечно, никто не запрещает написать так:

if DirectDrawCreateEx(nil, lpDD7, IID_IDIRECTDRAW7, nil) <> DD_OK then

Но профессионалы используют исключительно макросы FAILED() и SUCCEEDED().

После создания объекта необходимо установить режим кооперации - т. е. определить, оконный или полноэкранный режим будет использовать DirectDraw. Надо сразу "брать быка за рога" и программировать полноэкранные приложения. Режим кооперации учтанавливается методом IDirectDraw7.SetCooperativeLevel(). Первым параметром передаётся идентификатор окна типа HWND. Второй представляет собой комбинацию флагов. Установим такие:

  • DDSCL_ALLOWREBOOT - указывает, что будет доступно окно снятия программы по нажатию клавиш Ctrl-Alt-Del. Полезно при отладке приложения (вдруг зависнет). Этот флаг впоследствии можно и убрать.
  • DDSCL_EXCLUSIVE - используется только в полноэкранных приложениях.
  • DDSCL_FULLSCREEN - устанавливает полноэкранный режим. При его использовании обязателен флаг DDSCL_EXCLUSIVE.

Следующим действием является установка видеорежима. Это делает метод IDirectDraw7.SetDisplayMode(). Первые три параметра указывают ширину и высоту экрана, а также глубину палитры (8, 16, 24, 32). Последние два параметра используются редко. Подробности о них можно узнать из SDK.


Итак, мы создали объект DirectDraw и изменили разрешение экрана. В этот момент работы программы наше окно моментально заслоняет все объекты на Рабочем столе, включая панель задач. Если вы ещё не обратили внимание, для создания окна я использовал структуру WNDCLASSEX, а не WNDCALSS. Дело в том, что окно, построенное на базе последней, не сразу закрывает весь экран - 1-1,5 секунды продолжает красоваться Панель задач. Возможно, всё дело во флаге WS_EX_TOPMOST, который можно использовать только совместно с WNDCLASSEX.

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

Поскольку поверхности могут быть разных типов (не только первичными, но и внеэкранными), а метод для их создания только один - IDirectDraw7.CreateSurface(), необходимо каким-то образом указать, что мы хотим создать именно первичную поверхность. Как это сделать?

Обратите внимание, что первый параметр метода - это указатель на структуру TDDSURFACEDESC2 (кстати, в SDK 8 для С++ этим параметром нужно передавать адрес структуры TDDSURFACEDESC, а не TDDSURFACEDESC2. Это ошибка перевода заголовочных файлов C++ на Pascal: тем не менее, всё работает корректно). Именно через эту структуру DirectDraw узнает, какой тип поверхности нужно создать.

Перед использованием структуры распространённой практикой является заполнение её нулями - на случай всякого "мусора" в RAM, который может привести к ошибке во время выполнения. Я использую функцию ZeroMemory(), но можно и FillChar(). Следующим действием является заполнение поля dwSize этой структуры размером самой структуры. Это действие обязательно!

Поле dwFlags определяет, какие другие поля структуры мы будем использовать. По-видимому, это сделано для обеспечения наивысшего быстродействия - теперь DirectDraw не будет просматривать все поля структуры (а их очень много), а "заглянет" только в те, которые мы указали. Мы будем использовать поля ddsCaps и dwBackBufferCount. Укажем это флагами DDSD_CAPS и DDSD_BACKBUFFERCOUNT. Поле ddsCaps.dwCaps как раз и определяет тип создаваемой поверхности - флаги DDSCAPS_PRIMARYSURFACE, DDSCAPS_FLIP и DDSCAPS_COMPLEX сообщат DirectDraw, что мы хотим создать первичную комплексную поверхность с возможностью переключения буферов. Поле dwBackBufferCount указывает количество задних буферов, которые мы подключим к первичной поверхности - установим 1 (хотя можно подключить и больше).

Вызываем метод IDirectDraw7.CreateSurface(), указав вторым параметром нужный интерфейс. Третий параметр зарезервирован и не используется.

Теперь к первичной поверхности нужно "приаттачить" задний буфер. Этот буфер исключительно важен для самого механизма вывода изображений и необходим для избавления от мерцания спрайтов. Для создания заднего буфера используется структура TDDSCAPS. Очищаем её и задаём флаг DDSCAPS_BACKBUFFER. Методом IDirectDrawSurface.GetAttachedSurface() подсоединим задний буфер. Всё, инициализация DirectDraw закончена.

Теперь нужно создать внеэкранные поверхности и загрузить в них изображение. Внеэкранные поверхности представляют собой линейные области данных в видео- или системной памяти, которые предназначены для хранения графических данных. За всё это отвечает процедура LoadFiles(). Первым делом получаем идентификатор загруженного функцией LoadImage() растра. Я не буду её описывать, т. к. к DirectDraw она не имеет прямого отношения - за этим прошу в Help Win32 API. Скажу лишь, почему я применил именно эту функцию. Дело в том, что часто возникает необходимость загружать не только файлы формата bmp, но и файлы других форматов, например gif или JPEG. Обычно для их загрузки применяются различные библиотеки DLL (например, nViewLib.dll), результаты работы функций в этих библиотеках - величина типа HBITMAP. Её можно использовать в единообразном методе загрузки файлов различных форматов. Рекомендую использовать именно такой метод, если только вы не гуру в алгоритмах сжатия разных форматов файлов и не собираетесь самостоятельно писать функции по преобразованию сжатых файлов в растр - ведь DirectDraw подобных решений не предоставляет. К сожалению, недостаток такого способа в том, что он немного медленнен :-(. Впрочем, увеличенное время считывания должно сколь-нибудь существенно проявиться лишь при загрузке очень большого количества файлов (30-40 МБ и более).

Теперь рассмотрим процедуру CopyImage(). Первым параметром мы передаём адрес интерфейса IDirectDrawSurface - именно на на эту поверхность и будет загружен растр. Вторым - идентификатор загруженного растра. Вообще, работа функции CopyImage() базируется на GDI. После создания контекста и выбора в него растра мы получаем размеры этого растра функцией GetObject(). Теперь нужно создать поверхность DirectDraw c полученными шириной и высотой растра. За это отвечает ещё одна функция CreateSurface(). Механизм в ней похож на тот, что использовался для создания первичной поверхности, поэтому подробно я его описывать не буду. Обратите только внимание на флаг DDSCAPS_OFFSCREENPLAIN - он указывает, что мы хотим создать именно внеэкранную поверхность. К сожалению, нельзя динамически менять размер поверхности DirectDraw - для этого нужно заново создать её и загрузить данные. После успешного создания поверхности можно загрузить на неё растр - для этого существует удобный метод IDirectDrawSurface.GetDC(). Захватив контекст устройства, осуществляем копирование растра с одного контекста на другой, захваченный (а фактически на поверхность DirectDraw). Для этого используется функция GDI BitBlt(). Достоинством метода можно считать тот факт, что теперь на поверхности DirectDraw можно рисовать любыми функциями GDI, а не только BitBlt(), не прибегая к прямому доступу к памяти, в которой расположена поверхность - например, нарисовать линию или овал. А если возникнет потребность вывести текст? Так что отказываться от GDI пока ещё рано. Да, захваченный контекст нужно освободить методом IDirectDrawSurface.ReleaseDC().

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

Рассмотрим OnDraw() подробнее. Это - сердце алгоритма вывода изображений на экран.

Вывод изображения в полноэкранном режиме осуществляется методом IDirectDrawSurface.BltFast(). Этот метод должен вызываться для поверхности, созданной как задний буфер - именно на нём мы и будем рисовать. Этот метод максимально быстр, но ограничен функционально, например, он не в состоянии сжать по осям выводимое изображение. Первые два параметра - координаты левого верхнего угла вывода спрайта. Третий - поверхность, содержимое которой будем выводить. Четвёртый - адрес структуры типа TRect, которая будет определять область вывода поверхности - её инициализацию я сделал перед вызовом метода IDirectDrawSurface.BltFast(). Поэкспериментируйте с функцией SetRect() и посмотрите, как параметры в ней влияют на изменение в выводе графики. Пятым параметром метода передадим константу DDBLTFAST_WAIT - она нужна для корректного вывода на экран.

Описанные действия нужно повторить для каждого выводимого спрайта. Последним делом нужно переместить содержимое заднего буфера на первичную поверхность, содержимое которой, как я уже сказал, представляет собой текущее изображение на мониторе. Сделаем это методом IDirectDrawSurface.Flip(), указав первым параметром nil - ведь у нас только один задний буфер. Константа DDFLIP_WAIT нужна для корректного вывода на экран. Этот метод следует вызывать только для первичной поверхности. Перемещение заднего буфера на экран - один из самых быстрых методов DirectDraw.

Теперь изображение появится на экране. Можете подвигать спрайт при помощи клавиатуры - так интересней.

После нажатия на клавишу Escape происходит выход из цикла. Затем программа вызывает нижестоящую функцию ReleaseDirectDraw(), которая восстанавливает начальное разрешение и удаляет созданные поверхности и объект DirectDraw. Заметьте, что вызов _Release() для соответствующего интерфейса будет корректным только в том случае, если произошёл вызов _AddRef(). Вообще, говорят что вызовы эти необязательны, но я всё же использую их для полной корректности работы программ.

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


Author: Виктор Кода
Source: http://www.delphikingdom.com/asp/viewitem.asp?catalogid=1175