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

Реферат Курсовая Конспект

Почему я не рекомендую использовать DelphiX

Почему я не рекомендую использовать DelphiX - раздел Образование, Состоит из следующих компонентов Хочется Поделиться С Новичками Своим Мнением По Поводу Компонетов Delphix И П...

Хочется поделиться с новичками своим мнением по поводу компонетов DelphiX и почему я не рекомендую их использовать.

С одной стороны, DelphiX - это удобно – нет необходимости выполнять утомительный набор методов DirectX и длинных, как многоступенчатая ракета, констант наподобие DDENUMSURFACES_CANBECREATED. Однако давайте посмотрим – используется что-нибудь подобное в С++? Я не могу исследовать всю Сеть в поисках овета на такой вопрос, но, думается – нет. Почему?

Такие наборы классов – это нестандартный подход. Допустим, вы потратили изрядно своего времени и досконально изучили DelphiX. Так вот, изучив всё ЭТО, вы в итоге не изучили сам DirectX. Второе – изученные классы обладают многим и позволяют писать реальные программы, но всё равно этот подход очень негибок – вы ограничены тем, что уже сделано. На этот счёт у меня есть веский аргумент – это Direct3D. Вот тут DelphiX уж точно не даст развернуться как следует – и это характерно и для других компонент DirectX, пусть и в меньшей мере. Третье – немного, но снижается быстродействие. Четвёртое – в классах DelphiX кроме непосредственных вызовов методов интерфейсов DirectX используются ещё и собственные функции – кто даст гарантию, что в них нет ошибок?
В конце концов, именно за такие вот «примочки» Delphi не в почёте у С-программистов – они попросту надсмехаются над такими методами разработки программ. К сожалению, должен к ним присоединиться и я. Как же так, возмутятся многие. Компонентный подход – это ведь основа основ Delphi! Согласен, использование TMemo или TComboBox – это действительно удобный подход, да что там – превосходный, отличный подход! Но вот в случае с DirectX или чем-то подобным использовать такие средства разработки крайне нежелательно. Как бы вы отнеслись к компоненту TOpenGL? Или TWin32API? Вот так-то. DelphiX можно использовать как источник разных идей по реализации того или иного эффекта – перенося всё это в свою программу в виде отдельных функций или собственноручно написанных классов. Так что изучайте прямой API – для уверенности в завтрашнем дне и в собственной квалификации.

Собственно, сами примеры

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

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

К сожалению, даже в SDK нет примеров решения этого вопроса. Впрочем ответить на него не так уж сложно – нужно лишь понять, что хочет DirectDraw и как это преподнести.

Корень проблемы в том, что как только при копировании методом BltFast() часть поверхности выходит за край той поверхности, на которую она копируется (обычно задний буфер), вывод не осуществляется. В чём причина такого нелепого ограничения – думается, опять же в обеспечении наибольшего быстродействия. Например, вы планируете создать игру типа Tetris, а не скроллинговую стрелялку, и все ваши спрайты будут двигаться только в пределах экрана – но вот DirectDraw всё равно пришлось бы проверять их выход за границы, даже при отсутствии в этом необходимости. Хотя эту проблему можно было бы решить с помощью флагов при создании конкретной поверхности, но Microsoft этого не сделала. Ну что же, сделаем за неё эту работу.

Обратите внимание на четвёртый параметр метода IDirectDrawSurface7.BltFast() – это адрес структуры типа TRect. Для чего он нужен? Как известно, назначение струтуры TRect в GDI API – указание положения и размера какой либо области путём задания левого верхнего и правого нижнего угла. Так вот, эта структура позволяет указать DirectDraw о необходимости вывести не всё изображение спрайта, а лишь его часть:

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

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

Вот код, ответственный за вывод части изображения:

// Предполагаем, что края спрайта не выходят за границы экранаSetRect( rRect, 0, 0, SPRITE_WIDTH, SPRITE_HEIGHT ); // Проверяем выход краёв, и если такая ситуация имеет место, то корректируем// положение области копирования на поверхности спрайтаif nX < 0 then rRect.Left := - nX ;if nY < 0 then rRect.Top := - nY;if nX + SPRITE_WIDTH > SCREEN_WIDTH then rRect.Right := SCREEN_WIDTH - nX;if nY + SPRITE_HEIGHT > SCREEN_HEIGHT then rRect.Bottom := SCREEN_HEIGHT - nY;

Где nX и nY – координаты левого верхнего угла спрайта. При выводе надо не забыть скорректировать их:

nX + rRect.Left, nY + rRect.Top

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

Кстати, если необходимо вывести всю поверхность изображения, вместо адреса структуры следует передать nil – как это сделано для фона.

Scale
Иногда при выводе может понадобится растянуть или сжать объект по осям или просто увеличить или уменьшить его – для подобных эффектов DirectDraw предоставляет метод IDirectDrawSurface.Blt(). Он является хотя и более медленным, чем BltFast() – однако при этом более функционален. Так вот, мы снова будем указывать с помощью структуры TRect область вывода изображения – но уже на поверхности-приёмнике данных. Изменяя её размеры, можно добиться пропорционального или непропорционального изменения масштаба изображения по осям X и Y.

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

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

В GDI нет прямого способа вывести нерегулярный спрайт – для этого необходимо подготовить изображение и отдельно маску. В чём недостатки такого подхода? Их много. Во-первых, требуется иметь два изображения – это увеличивает объём данных в памяти и на диске. Во-вторых, скорость вывода данного изображения падает вдвое, а ведь GDI и без этого не славится своей скоростью. В третьих, это дополнительная забота того, кто готовит изображение в графическом редакторе. В своё время автор даже написал небольшую утилиту, которая создавала маску для выбранного изображения и записывала её в отдельный файл. Но теперь об этом можно забыть.

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

Следующий фрагмент кода создаёт и присоединяет к поверхности «цветовой ключ», цвет которого – чёрный.

var ddck: TDDCOLORKEY; ddck.dwColorSpaceLowValue := 0;ddck.dwColorSpaceHighValue := ddck.dwColorSpaceLowValue; pSprite.SetColorKey( DDCKEY_SRCBLT, @ddck );

Для указания прозрачного цвета, как видно, используется структура TDDCOLORKEY. В её двух полях необходимо указать нижнюю и верхнюю границу диапазона «прозрачных» цветов. Замечу, что использование диапазона цветов возможно только в случае, если такая возможность поддерживается аппаратно. Поэтому лучше ограничиться каким-либо одним цветом, как это сделано выше. После заполнения структуры TDDCOLORKEY необходимо вызвать метод IDirectDrawSurface7.SetColorKey(), где первый параметр – один из возможных флагов, второй - адрес структуры TDDCOLORKEY. Обычно используется флаг DDCKEY_SRCBLT, который указывает, что при копировании изображения будет использоваться цветовой ключ поверхности-источника. Другие флаги можно узнать из справочной службы DirectX SDK.

Теперь о главном. В приведённом выше фрагменте кода в качестве маски задаются пиксели чёрного цвета. Как известно, нулевое значение обозначает отсутствие цвета во всех графических режимах – 16 цветов, 256, 65535 и т.д.

Поэтому можно смело присваивать 0 для чёрной маски в любом режиме. Однако, предположим, нам надо задать цветовой ключ в виде чистого синего цвета. Для 24- и 32-битного режима это можно сделать с помощью макроса (функции) из модуля windows.pas:

function RGB(r, g, b: Byte): COLORREF;begin Result := (r or (g shl 8) or (b shl 16));end;

Зарезервированное слово shl относится к сдвиговым операциям и сдвигает содержимое на указанное значение влево.

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

Так, для задания цветового ключа в виде чистого синего цвета необходимо написать так:

ddck.dwColorSpaceLowValue := RGB( 0, 0, 255 );ddck.dwColorSpaceHighValue := ddck.dwColorSpaceLowValue;

Ну и в том же духе, в полном соответствии с теорией цвета.

А теперь попробуйте задать цветовой ключ для 16-битового режима. Ничего не получится. Почему? Дело в том, что цвет пикселя хранится в ячейке длиной в 16 бит, а цветовых составляющих – 3, появляется лишний бит, который чаще отдаётся зелёному цвету, а иногда просто не используется. Формат, где теряется лишний бит, обозначается 5-5-5 (на каждую цветовую составляющую по пять бит, а не одному байту), другой формат обозначается 5-6-5 (на зелёную составляющую выделяется 6 бит ). Понятно, что задание цвета с помощью функции RGB() для таких форматов ни к чему ни приведёт.

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

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

var ddpf: TDDPIXELFORMAT; ZeroMemory( @ddpf, SizeOf( TDDPIXELFORMAT ) );ddpf.dwSize := SizeOf( TDDPIXELFORMAT ); pSprite.GetPixelFormat( ddpf );

Формат цветовых составляющих описывается в полях dwRBitMask, dwGBitMask и dwBBitMask структуры TDDPIXELFORMAT – но только в том случае, если битовое поле dwFlags содержит флаг DDPF_RGB – признак того, что поверхность создана в RGB-режиме. Значения полей dwRBitMask, dwGBitMask и dwBBitMask для режимов с разной глубиной палитры описываются в разделе dwRBitMask, dwGBitMask и dwBBitMask справочной службы DirectX SDK:

DDPF_RGB 16 R: 0x0000F800 G: 0x000007E0 B: 0x0000001F A: 0x00000000
DDPF_RGB 16 R: 0x0000001F G: 0x000007E0 B: 0x0000F800 A: 0x00000000
DDPF_RGB 16 R: 0x00007C00 G: 0x000003E0 B: 0x0000001F A: 0x00000000

Запустите готовое приложение GetPixFormat из каталога DXCommon – и посмотрите, какой формат поверхности использует ваша карта в 16-битовом режиме. Скажу, что на компьютере с видеоакселератором GeForce 2 MX 420 получались значения из самой верхней ячейки – и это соответствует формату 5-6-5. По-моему, именно такой формат принят во всех современных видеокартах (заметьте, что во второй ячейке таблицы составляющие R и B переставлены местами). А вот, например, дедушка S3 Trio 3D/2X использует формат, описанный в нижней ячейке – опытным путём установлено, что это 5-5-5.

Вот как должен быть переписан макрос для формата 5-6-5:

function RGB565( r, g, b: Byte ): COLORREF;begin Result := ( ( r shl 11 ) or ( g shl 5 ) or b ); end;

Графически битовая маска может быть представлена так:

А вот как должен выглядеть макрос для формата 5-5-5:

function RGB555( r, g, b: Byte ): COLORREF;begin Result := ( ( r shl 10 ) or ( g shl 5 ) or b ); end;

Графически битовая маска может быть представлена так:

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

Естетственно, что теперь максимальное значение, передаваемое в макросы (функции) RGB565() и RGB555(), соответствует значению 31, а для задания читого зелёного цвета в режиме 5-6-5 необходимо указать RGB565( 0, 63, 0 ), т. к. битов 6.

Для того, чтобы наша DirectDraw-программа без проблем работала в обоих форматах, необходимо проверить текущий формат, запомнить его и при задании цветового ключа для поверхности вызвать соответствующий макрос. Всё это и делается в приложении Transparent – надеюсь при его разборе у вас не возникнет проблем. Не забудьте при копировании методом BltFast() указать флаг DDBLTFAST_SRCCOLORKEY.

Fps
Ещё один полезный пример – вывод текста на поверхность DirectDraw – в виде значения fps. Сама компонента не обладает такими средствами – DirectDraw изначально создавался лишь для максимально быстрого копирования одного изображения на другое. Для вывода текста необходимо использовать GDI.

Как неоднократно упоминалось, GDI очень медленнен, и вывод текста – одна из функций, которая серьёзно может «притормозить» DirectDraw-программу. Поэтому необходимо пользоваться этой функцией как можно реже. Для взаимодействия DirectDraw c GDI введён простой метод IDirectDrawSurface7.GetDC(). Получив контектс, можно спокойно чертить в нём всеми мыслимыми функциями GDI. Метод IDirectDrawSurface7.ReleaseDC() переносит содержимое контекста в область памяти, занятую поверхностью DirectDraw и удаляет контекст.

Откройте файл проекта fps.dpr. Т. к. функция TextOut() уже занята, функцию, отвечающую за вывод текста, пришлось назвать менее звучно – OutText(). Я не буду подробно описывать её, надеюсь, всё понятно. Для ускорения работы программы я поступил так: для вывода текста используется отдельная поверхность – именно на неё и выводится текст средствами GDI. Затем всё время поверхность просто копируется на задний буфер – это осуществляется гораздо быстрее, чем постоянный вывод текста на задний буфер, а когда появляется необходимость изменить текст – он снова выводится на нашу отдельную поверхность. Потребность изменить текст появляется лишь раз в секунду.

Для вызова OutText() я использовал мультимедиа-таймер Windows. Значение fps наращивается при каждом построении кадра и обнуляется после вызова OutText().

И последнее. По-видимому, в операционной системе Windows 2000 функции GDI должны работать быстрее, т. к. эта ОС полностью 32-х разрядная. Но всё же рекомендую пользоваться описанным выше подходом.

Text
Ещё один пример вывода текста – но уже на задний буфер. Добавлен мною для полноты темы. Текст заданных размеров постоянно выводится на задний буфер без поверхности-посредника. При выводе текста я столкнулся с одной проблемой – это сглаживание краёв символов. В модуле windows.pas описана константа ANTIALIASED_QUALITY, но её задание в параметре fdwQuality функции CreateFont() ни к чему ни привело. Может быть, в Windows 9x и МЕ это значение не используется? Во всяком случае, константы ANTIALIASED_QUALITY и NONANTIALIASED_QUALITY в справке Delphi Help не описаны.

Sound
Эта программа – прямое продолжение моего первого примера по использованию DirectSound. Введено ряд усовершенствований:

  1. Файл lowfunc.pas теперь полностью закончен и является практически прямым переводом файла wavread.cpp. Выражаю благодарность Max Morozov и iXania, которые помогли мне перевести некоторые сложные конструкции с языка C++ на Object Pascal, т. к. самому мне для этого не хватило квалификации. Теперь нет необходимости использовать отдельную динамическую библиотеку – весь код располагается в exe-файле. Всем спасибо.
  2. Я решил написать небольшой класс TWave – он сам заботится об открытии звукового файла, чтении данных из него в звуковой буфер и проигрывании их. Функциональность класса не полная – это лишь пример. Благодаря ООП главный модуль main.pas серьёзно уменьшился, теперь для воспроизведения wav-файла средствами DirectSound достаточно написать:
3. var sound: TWave;4. 5. sound := TWave.Create();6. sound.OpenWaveFile( ‘wavefile.wav’ );sound.Play();

Правда, просто?

GetDXVer и GetDXVerSetup
Я решил заглянуть в некоторые области DirectX, до которых руки многих авторов книг по DirectX попросту «не доходят».

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

Первый пример, который я предлагаю вашему вниманию – это GetDXVer. Это аналог из DirectX SDK для Visual C++. Функция GetDXVersion() ответственна за получение намера текущей версии DirectX. Каким образом она действует? Механизм прост, но достаточно громозд. Сначала загружается нужная динамическая библиотека из комплекса DirectX, например DDRAW.DLL или DINPUT.DLL. Затем получают адреса функций, которые экспортируют эти библиотеки – это «создающие» функции наподобие DirectDrawCreate() или DirectInputCreateA(). Затем при помощи этих функций и создаются нужные интерфейсы вроде IDirectDraw и т.п. Если на каком-то шаге происходит сбой, это означает, что данная функция или интерфейс не поддерживаются. Зная, в какой версии появился тот или иной интерфейс, можно выяснить текущую версию DirectX.

Ещё одна функция, GetDXRegVersion(), извлекает полный порядковый номер из реестра Windows. Кстати, эта функция может в принципе читать любой строковый параметр из реестра и делает ненужным использование класса TRegistry, что очень важно, если мы хотим получить маленький по размерам исходный модуль.

Пример имеет два недостатка:

  1. Работает с некоторой задержкой. Для создания всех интерфейсов требуется некоторое время. Особо медленно создаётся интерфейс IDirectMusic.
  2. Программа не способна определить номер версии, если он выше 8 – это принципиальный барьер.

Ещё один пример – GetDXVerSetup – использует специальную функцию DirectXSetupGetVersion() из библиотеки dsetup.dll. эта библиотека не входит в стандартный run-time DirectX, а поставляется только с setup-программами установки DirectX на компьютер пользователя. При написании этого примера я столкнулся с двумя проблемами:

  1. В Help-службе DirectX SDK 7 указаны такие возможные значения, которые могут быть помещены в переменную pdwVersion при вызове функции DirectXSetupGetVersion():
DirectX version Value pointed to by pdwVersion
DirectX 1 0x00040001
DirectX 2 0x00040002
DirectX 3 0x00040003
DirectX 5.0 0x00040005
DirectX 6.0 0x00040006
DirectX 7.0 0x00040007
  1. А вот в Help-службе DirectX SDK 8 указаны такие:
DirectX version Value pointed to by pdwVersion
DirectX 1 0x00000000
DirectX 2 0x00000000
DirectX 3 0x00000000
DirectX 5.0 0x00040005
DirectX 6.0 0x00040006
DirectX 7.0 0x00040007
DirectX 8.0 0x00040008
  1. Зачем понадобилось обозначить версии 1, 2 и 3 как отсутствие DirectX – непонятно. Может быть, Microsoft посчитала, что эти ранние версии уже слишком устарели и не обеспечивают пользователя нужными мультимедиа-средствами? А раз так, то может быть лучше вообще известить об отсутствии DirectX? Может статься, что это просто ошибка в файле справки. Второй вариант более правдоподобен, первый – забавен.
  2. Используя файл directsetup.pas, мне не удалось экспортировать функцию DirectXSetupGetVersion() из библиотеки dsetup.dll. Дело в том, что она ищется в каталоге, по-видимому, указанном в ключе реестра HKEY_LOCAL_MACHINESoftwareMicrosoftWindowsCurrentVersionUninstallDirectXDrivers, но я не обнаружил такой ключ в своём реестре. Так что воспользоваться этим файлом не представилось возможным. К тому же структуры TDirectXRegisterAppW2 не существует – приехали!

Я самостоятельно перевёл файл dsetup.h из SDK 8 в файл dsetup.pas, пытаясь максимально точно соблюдать синтаксис структур и параметро функций. Может быть, кто-то им воспользуется.

Недостаток приведенного метода в том, что вам придётся постоянно «таскать» библиотеку dsetup.dll вместе с исходной программой.

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

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

Напоследок хочу выразить особую благодарность Антону Ржешевскому за его дельные советы в освоении DirectX и не только.

DirectX для начинающих.

 

Часть третья. Считывание и запись

 

Вернуться к разделу Hello, World! Автор Виктор Кода, дата публикации 15 июля 2002г.
  • DirectX для начинающих
  • DirectX для начинающих.Часть вторая

Привет всем, кто интересуется программированием под DirectX на языке Object Pascal!

Как и обещал, я продолжаю искать новый материал по DirectX, переводить его на язык Object Pascal и представлять всеобщему вниманию. Недавно у меня появилась идея снятия скриншотов с экрана DirectDraw-программы и записи изображения в простой bmp-файл - некоторые игры позволяют это делать, и я решил последовать их примеру. Потом я наткнулся на другой интересный материал - речь шла о загрузке изображения из bmp-файла без использования функции LoadImage(). Поэтому тема статьи всецело посвящена работе с bmp-файлами на "низком уровне". Замечу, что это немного сложные вещи, но мы ведь сложностей не боимся, правда? Иначе непонятно, зачем тогда заниматься изучением DirectX вообще.

Замечания

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

Немного изменилась реализация модуля ddutils.pas - теперь функция CreateSurface() не требует адрес главного интерфейса IDirectDraw, а создаёт и уничтожает локальный интерфейс. Возможно, более практично с точки зрения Pascal-программирования было бы объявить глобальный для модуля ddutils.pas интерфейс IDirectDraw, а для создания и удаления интерфейса воспользоваться секциями initialization и finalization модуля.

Также немного изменился стиль написания программ, теперь он совсем грешит С-подобным кодом :-)

FASTFILE1

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

  1. С помощью функции LoadImage() загружался растр и нам сообщался идентификатор загруженного растра в виде переменной типа HBITMAP;
  2. Функцией SelectObject() в контекст-источник выбирался созданный растр;
  3. Методом IDirectDrawSurface7.GetDC() создавался GDI-совместимый дескриптор контекста-приёмника и осуществлялась блокировка поверхности;
  4. Функцией GDI BitBlt() содержимое контекста-источника копировалось на контекст-приёмник.
  5. Методом IDirectDrawSurface7.ReleseDC() удалялся созданный контекст и осуществлялась разблокировка поверхности.

В чём недостатки такого подхода? В универсальности. Не скажу, что функция LoadImage() очень медленная, но и очень быстрой она не является. Во всяком случае, программисты, писавшие её, не ставили своей задачей обеспечить максимальную скорость загрузки. Посмотрите справку по этой функции (файл win32sdk.hlp) - обилие параметров и констант, задаваемых при вызове, наводят на мысль о том, что она довольно "тяжеловесна". В частности, сказано, что с её помощью можно загружать не только растры из bmp-файлов разных форматов (в их число входят монохромные, 16- и 256-цветные палитровые файлы, а также беспалитровые 24-битные файлы), но и файлы иконок Windows и даже файлы, содержащие изображения курсоров.

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

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


Рис.1. Структура файла BMP, не содержащего палитру

Хранящийся на диске файл DIB, обычно с расширением .bmp, как видно, начинается со структуры BITMAPFILEHEADER, позволяющей начать работу с файлом. Вот как эта структура описана в файле windows.pas:

tagBITMAPFILEHEADER = packed record bfType: Word; // Тип файла. Должен содержать 'BM' ($4d42) bfSize: DWORD; // Размер файла в байтах bfReserved1: Word; // Зарезервировано, должен быть нуль bfReserved2: Word; // Зарезервировано, должен быть нуль bfOffBits: DWORD; // Смещение от начала файла до гафических данныхend;BITMAPFILEHEADER = tagBITMAPFILEHEADER;

Следом за структурой BITMAPFILEHEADER следует стуктура BITMAPINFO:

tagBITMAPINFO = packed record bmiHeader: TBitmapInfoHeader; // Структура BITMAPINFOHEADER bmiColors: array[0..0] of TRGBQuad; // RGB-триплексend; BITMAPINFO = tagBITMAPINFO;

Фактически, стуктура BITMAPINFO включает в себя ещё одну структуру - BITMAPINFOHEADER:

tagBITMAPINFOHEADER = packed record biSize: DWORD; // Размер самой структуры в байтах biWidth: Longint; // Ширина растра в пикселях biHeight: Longint; // Высота растра в пикселях biPlanes: Word; // Количество плоскостей (всегда 1) biBitCount: Word; // Количество бит на 1 пиксель biCompression: DWORD; // Тип сжатия (BI_RGB - без сжатия) biSizeImage: DWORD; // Размер изображения в байтах (обычно 0) biXPelsPerMeter: Longint; // А эти данные biYPelsPerMeter: Longint; // нам вообще biClrUsed: DWORD; // никогда не biClrImportant: DWORD; // понадобятся :)end;BITMAPINFOHEADER = tagBITMAPINFOHEADER;

Эта структура для нас наиболее интересна, так как опираясь на её данные, и будет производиться загрузка растра. Несмотря на обилие полей, нам понадобятся только некоторые - это biWidth, biHeight и ещё поле biBitCount - для проверки, является ли файл 24-битным.

После этих структур начинаются графические данные. В 24-битном файле каждый пиксель кодируется 3 байтами - на каждую составляющую R, G, B - по одному байту. Значение каждой составляющей может варьироваться от 0 до 255.

Откройте файл проекта и найдите функцию LoadData(). Она вызывает другую функцию - LoadBitMap(). Я разместил её в файле ddutils.pas, вот её прототип

function LoadBitMap( name: pchar; pbi: PBITMAPINFO ): pointer;

Первым параметром передаётся имя загружаемого файла, вторым - адрес структуры BITMAPINFO, структура понадобится после вызова функции LoadBitMap().

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

Вот как, например, открывается файл:

hFile := CreateFile( name, GENERIC_READ, FILE_SHARE_READ, nil, OPEN_EXISTING, 0, 0 );if hFile = INVALID_HANDLE_VALUE then exit;

Переменная hFile - это дескриптор открытого файла. Проверить, открыт ли он в самом деле можно, сравнив дескриптор с константой INVALID_HANDLE_VALUE. Далее считывается структура BITMAPFILEHEADER:

ReadFile( hFile, bfh, sizeof( BITMAPFILEHEADER ), dwBytesRead, nil );

Замечу, что вторым параметром функции ReadFile() передаётся сама структура, куда будут записаны данные, третьим - количество байт, которые надо прочитать. Четвёртые параметр должен присутствовать обязательно, в него функция запишет количество реально прочитанных байт. Для пущей надёжности можно сравнить это значение с размером структуры BITMAPFILEHEADER, и если значения не совпадают, объявить об ошибке.

Далее считывается структура BITMAPINFOHEADER:

ReadFile( hFile, bi, sizeof( BITMAPINFOHEADER ), dwBytesRead, nil );

Думаю, надо объяснить, почему здесь мы читаем только данные структуры BITMAPINFOHEADER, и не считываем массив bmiColors. Дело в том, что этот массив в структуре BITMAPINFO, там, куда мы её передадим позже, всё равно не используется. Однако он входит в состав общих графических данных, поэтому мы считаем его вместе с ними, а в структуре bi массив bmiColors оставим пустым.

Далее идёт считывание графических данных. Прежде всего необходимо определить, какой размер они имеют:

// Определяем размер DIBdwDIBSize := GetFileSize( hFile, nil ) - sizeof( BITMAPFILEHEADER ) - sizeof( BITMAPINFOHEADER );

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

// Выделяем участок памятиresult := pbyte(GlobalAllocPtr( GMEM_MOVEABLE, dwDIBSize ));

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

// Читаем данные DIBReadFile( hFile, result^, dwDIBSize, dwBytesRead, nil );// Закрываем файлCloseHandle( hFile );

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

Итак, функция LoadBitMap() загрузила в оперативную память битовые данные, формирующие изображение и вернула указатель на них как результат функции. Вернёмся теперь обратно к функции LoadData(). Первый шаг сделан - произведена максимально быстрая загрузка данных из файла (я не вижу способа, как можно ещё как-нибудь ускорить этот процесс). Теперь надо сделать второй шаг. В чём он состоит? Для ускорения загруки в играх и других программах все графические данные объединяются в один или несколько больших файлов. Такую реализацию, например, можно увидеть в игре Donuts из DirectX SDK 7-8. Такое объединение очень полезно при условии, что файл на жестком диске нефрагментирован. Данный метод, безусловно, уменьшает время загрузки, но как будет видно далее, добавляет лишних хлопот программисту.

Я подготовил простой bmp-файл, в котором хранится изображение для фона и десять "кадров", которые будут последовательно сменять друг друга. Как же загрузить эти данные на поверхности DirectDraw? Есть два пути:

  1. Воспользоваться методами Lock() и Unlock() интерфейса IDirectDrawSurface7, и осуществлять прямое копирование данных функцией CopyMemory(). Это решение оптимально по скоростным характеристикам, но уж очень сложное: необходимо учитывать абсолютно все нюансы формата поверхности, на которую копируются данные - а их очень много, ведь формат меняется в зависимости от глубины цвета текущего видеорежима - 8, 16, 24, 32 бит. К тому же, выигрыш в этом случае может оказаться совсем небольшим.
  2. Использовать функцию GDI StretchDIBits(). Она предназначена для копирования в контекст данных, расположеных не в другом контексте, а находящихся просто в указаннном участке памяти. Может сложиться впечатление, что эта функция достаточно медленна - из-за угрожающей приставки "Stretch". Однако если будут копироваться участки битов, одинаковые по высоте и ширине, то в этом случае функция, думается, должна работать быстрее.

Я решил использовать второй способ. Итак, первым делом создадим поверхность для фона:

CreateSurface( g_pWallpaper, 640, 480 );

После этого получим контекст для поверхности и осуществим копирование функцией StretchDIBits(). В файле справки о методе IDirectDrawSurface7.GetDC() сказано, что он является надмножеством над методом IDirectDrawSurface7.Lock() - т. е. осуществляет те же операции, которые мы бы проделали при прямом копировании данных. Различие в том, что здесь DirectDraw учитывает особенности формата поверхности при создании контекста-приёмника. Думаю, нет необходимости дублировать эти операции - выигрыш в скорости может оказаться весьма сомнительным, т.к. код в библиотеке DirectDraw и без того максимально быстр.

if g_pWallpaper.GetDC( DC ) = DD_OK then begin// Копируем битовый массив в контекст StretchDIBits( DC, 0, 0, 640, 480, 0, 64, 640, 480, pBits, bi, 0, SRCCOPY ); g_pWallpaper.ReleaseDC( DC ); end;

Заметьте, что растр в файле (и памяти) хранится в перевёрнутом виде, поэтому ось Y битовой карты направлена вверх. Это необходимо учитывать при задании области копирования. Для копирования массива битов функции StretchDIBits() необходимо передать адрес массива в памяти, а также адрес структуры BITMAPINFO - опираясь на неё, она сможет правильно произвести копирование.

Далее 10 раз осуществляется копирование в отдельные поверхности массива g_pMovie. Опять же, необходимо учитывать, что строки растра перевёрнуты. После этого необходимо освободить участок системной памяти, где хранится битовый массив:

// Освободили битовый массив!pBits := nil;

Вот и всё, можно приступать к отрисовке экрана.

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

FASTFILE2

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

Чтобы не копировать каждый раз содержимое нового участка памяти на отдельную поверхность DirectDraw функцией StretchDIBits(), я решил все данные из памяти скопировать на одну большую поверхность DirectDraw, а потом копировать её содержимое по участкам на другие поверхности методом IDirectDrawSurface7.BltFast(). Казалось бы, такое двойное копирование - из памяти на общую поверхность, а потом с этой поверхности на отдельные поверхности - довольно долгий процесс. Однако если память видеокарты достаточно большая (32-64 Мб), можно позволить программе разместить все созданные поверхности в памяти видеокарты, и тогда копирование методом IDirectDrawSurface7.BltFast() будет происходить очень быстро. При большом объёме графических данных этот способ предпочтителен. К тому же данные на общей поверхности DirectDraw хранятся в нормальном, а не перевёрнутом виде, что облегчает программисту перенос.

Этот способ и демонстрирует данный проект. Всё остальное осталось без изменений.

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

Например:

g_pBackBuffer.BltFast( x, y, g_pMovie[ frame ], nil, DDBLTFAST_WAIT );

Однако можно третьим параметром указать общую data-поверхность, а четвертым - не nil, а область на этой поверхности:

g_pBackBuffer.BltFast( x, y, g_pDataSurface, arrayRect[ FRAME_01 ], DDBLTFAST_WAIT );

Тогда можно не создавать отдельные поверхности и не заниматься копированием данных. Однако есть и недостатки. Например, память видеокарты должна быть достаточно большой - если памяти не хватит для размещения всей data-поверхности, DirectDraw разместит её в системной памяти, и процесс вывода изображения резко замедлится - вот вам и оптимизация! Также могут возникнуть проблемы с цветовыми ключами и корректным отображением спрайтов. В общем, решение половинчатое.

PRINTSCREEN

Заманчиво, когда в программе имеется возможность делать "снимки" экрана и сразу записывать их в файл. Этот пример ничем не отличается от предыдущих, за исключением того, что при нажатии на клавишу "Пробел" делается запись содержимого экрана в файл screen.bmp.

Функция, которая проделывает эту работу, находится в файле pscreen.pas. Рассмотрим её.

Первым делом создаётся новый файл или открывается для перезаписи старый:

// создаём файл с заданным именем, в него будет производиться записьhFile := CreateFile( szFileName, GENERIC_WRITE, FILE_SHARE_READ, nil, CREATE_ALWAYS, 0, 0 );if hFile = INVALID_HANDLE_VALUE thenbegin CloseHandle( hFile ); exit;end;

Затем нам необходимо получить данные о поверхности (здесь в функцию передан дополнительный буфер):

// подготавливаем структуру TDDSURFACEDESC2ZeroMemory( @ddsd2, sizeof( TDDSURFACEDESC2 ) );ddsd2.dwSize := sizeof( TDDSURFACEDESC2 ); // получаем формат поверхностиpSurface.GetSurfaceDesc( ddsd2 );dwWidth := ddsd2.dwWidth;dwHeight := ddsd2.dwHeight;dwBPP := ddsd2.ddpfPixelFormat.dwRGBBitCount;

Структура ddsd2 используется дополнительно в методе Lock() поверхности. Заблокировав поверхность, можно обратится к её содержимому для чтения данных:

// блокируем поверхность DirectDrawif( FAILED( pSurface.Lock( nil, ddsd2, DDLOCK_WAIT, 0 ) ) ) then exit;

Затем необходимо выделить достаточное количество памяти под массив пикселей. Число три в конце выражения - это потому, что вывод будет осуществляться в 24-битный файл:

pPixels := pbyte(GlobalAllocPtr( GMEM_MOVEABLE, dwWidth * dwHeight * 3 ));

Затем начинается главное. Т. к. формат пикселя поверхности в каждом из графических режимов различается, необходимо предусмотреть все особенности размещения данных. Бессмысленно подробно описывать все операции - они запутанны и сложны. Мне понадобилось некоторое количество времени, чтобы правильно перевести все операции с указателями с языка C++ в контекст Object Pascal. Операции с указателями на этом языке получаются довольно путаными, малейшая оплошность приводит к тому, что обычно в файл записывается не тот участок памяти (получается мешанина из пикселей), или запись вообще не происходит. Обратите внимание на такую строку:

pixel := PDWORD(DWORD(ddsd2.lpSurface) + i * 4 + j * ddsd2.lPitch)^;

Здесь определяется цвет нового пикселя поверхности. ddsd2.lpSurface - это указатель на начало данных поверхности, а ddsd2.lPitch - шаг поверхности, учитывать его нужно обязательно.

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

Для начала необходимо вручную подготовить структуры BITMAPFILEHEADER и BITMAPINFOHEADER. В последней надо указать ширину и высоту растра, а также разрядность пикселя. Тип сжатия должен быть BI_RGB - т. е. без сжатия.

После этого с помощью API-функций Windows последовательно в файл записываются структуры BITMAPFILEHEADER, BITMAPINFOHEADER и далее - подготовленные данные из памяти. После записи файл необходимо закрыть, а память - освободить:

// закрываем файлCloseHandle( hFile );pPixels := nil;

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

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

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

 

DirectInput API

 

Вернуться к разделу Hello, World! DirectX для начинающих Автор Виктор Кода, дата публикации 8 апреля 2002г.

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

Последовательность действий при использовании DirectInput для получения пользовательского ввода в упрощённом виде может быть представлена следующими шагами:

  • Создание объекта DirectInput (интерфейс IDirectinput8).
  • Создание устройства (получения интерфейса IDirectInputDevice8, который будет инкапсулировать свойства устройства ввода).
  • Установка режима совместного доступа для выбранного устройства.
  • Установка формата данных для выбранного устройства.
  • В случае работы с буферизированным опросом задание размера буфера для хранения данных.
  • Захват устройства
  • Получение данных методом IDirectInputDevice.GetDeviceState().
  • По окончании работы освобождение устройства и удаление объектов DirectInput.

Для возможности пользоваться методами DirectInput необходимо для начала создать главный объект DirectInput. Для версии 8 необходимо вызывать функцию DirectInput8Create() а не DirectInputCreate() или DirectInputCreateEx(), как это было в предыдущих версиях. Первым параметром необходимо передать дескриптор экземпляра программы - его можно получить, вызвав GetModuleHandle( 0 ). Второй параметр - номер версии DirectInput. Обычно передаётся DIRECTINPUT_VERSION, но для совместимости с ранними версиями можно указать и другое значение, например $0300. Параметр riidltf может принимать только одно значение - IID_IDIRECTINPUT8. Четвёртый параметр является указателем на интерфейс IDirectInput8. Последний параметр служит для агрегирования COM и не используется.

Если этот шаг одинаков для работы со всеми типами устройств ввода, то дальнейшие будут немного различаться в чертах. Для работы с каким-либо типом устройства ввода необходимо создать объект, который будет инкапсулировать в себе свойства этого устройства. Делается это методом IDirectInput.CreateDevice(). Первым параметром передаётся GUID экземпляра устройства. Этот идентификатор может быть получен, например, перечислением установленных в компьютере устройств ввода. Кроме того, можно использовать предопределённый GUID. Для каждого типа устройства определён свой GUID в файле DirectInput8.pas - для клавиатуры это GUID_SysKeyboard, для мыши - GUID_SysMouse и т. д. Вторым параметром передаётся указатель на интерфейс IDirectInputDevice8. Третий параметр не используется.

Следующий шаг не требует серьёзных раздумий и фактически является обязательным - это определение формата данных для созданного устройства методом IDirectInputDevice8.SetDataFormat(). Формат структуры даст DirectInput знать об особенностях опрашиваемого устройства ввода. Для каждого типа существует предопределённый формат - ими мы и будем пользоваться. В крайне редких случаях нужно задавать собственный формат - для нестандартных устройств ввода. Для клавиатуры воспользуемся структурой c_dfDIKeyboard - загляните в заголовочный файл - её поля определяются довольно сложным образом. Для мыши следует указать структуру c_dfDIMouse. Учтите, что в метод IDirectInputDevice8.SetDataFormat() нужно передать адрес структуры.

Следующим в списке действий является установка уровня совместного доступа - делается это методом IDirectInputDevice8.SetCooperativeLevel(). Первый параметр - дескриптор главного окна программы. Второй - комбинация флагов, которые и определяют уровень доступа. Используются следующие флаги:

  • DISCL_FOREGROUND - программа может получать данные от устройства ввода, только будучи активной.
  • DISCL_BACKGROUND - опрос будет возможен даже в неактивном состоянии.
  • DISCL_EXCLUSIVE - программа получит монопольный доступ к устройству, никакое другое приложение не сможет получить монопольный доступ, хотя совместный доступ может быть получен.
  • DISCL_NONEXCLUSIVE - программа получит совместный доступ, другие приложения также будут иметь доступ к устройству ввода.
  • DISCL_NOWINKEY - блокируется клавиша Windows.

Фактически установка уровня кооперации состоит из двух этапов: необходимо указать тип доступа к устройству ввода - активный(foreground) или фоновый (background), а также назначить режим работы с выбранным устройством - монопольный (exclusive) или совместный (nonexclusive).

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

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

Последний флаг, DISCL_NOWINKEY, может применяться совместно с DISCL_NONEXCLUSIVE, в режиме DISCL_EXCLUSIVE клавиша Windows блокируется автоматически.

Для успешного получения данных от устройства ввода необходимо "захватить" его методом IDirectinputDevice8.Acquire().

Для получения собственно данных необходимо вызвать метод IDirectInputDevice8.GetDeviceState() или IDirectInputDevice8.GetDeviceData().

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

Keyboard

Этот пример демонстрирует работу DirectInput для опроса клавиатуры. Для наглядности я включил в программу возможность выбора метода опроса клавиатуры - методом DirectInput и стандартным Windows Messaging. Вы сразу почувствуете разницу в скорости работы и убедитесь в том, что методы, пригодные для набора текста с клавиатуры, мало приспособлены для управления в быстро меняющейся обстановке - например, в играх. Причина том, что сообщение WM_KEYDOWN обрабатывается наравне с десятками других, которыми операционная система заваливает ваше окно, и не только ваше, но и все другие - а их очень много даже тогда, когда на экране и в панели задач не видно ни одного. К тому же Windows Messaging не в состоянии определить нажатие двух и более клавиш одновременно - и это тоже большой недостаток. Правды ради нужно сказать, что в Windows существует стандартная функция GetKeyboardState() - она общается напрямую с драйвером клавиатуры и работает не медленнее, чем DirectInput, чем немного умаляет достоинства этой компоненты. Впрочем, при создании DirectInput все усилия разработчиков были направлены на поддержку игровых контроллеров самых разных типов и других нестандартных устройств ввода - и здесь возможности Windows Messaging весьма скромны.

После создания объекта DirectInput8, объекта для работы с клавиатурой и т. д. всё готово для опроса клавиатуры - он производится из процедуры TForm1.Idle() постоянным вызовом функции UpdateKeyboardState(). Рассмотрим её подробнее.

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

Заметьте, что я не проверил возвращённое значение макросом FAILED() - ошибочный результат может быть связан не только с неправильной инициализацией объектов DirectInput, но и в случае с так называемой "потерей" устройства ввода. Конечно, само устройство никуда не теряется, а остаётся на столе или в руках пользователя - но вот получение данных от него прекращается. Трудно сказать, почему это происходит, но это так. В этом случае необходимо сравнить возвращенное значение с DIERR_INPUTLOST и если это так, то снова захватить устройство. Если при повторном опросе снова возникает ошибка, то прекращаем работу - может быть, устройство отсоединили от компьютера или вообще выключили.

Как же узнать состояние клавиш клавиатуры? После удачного опроса массив из элементов типа Byte оказывается заполненным в соответствии с кодами нажатых клавиш. Элемент массива со значением $080 соответствует нажатой клавише, иначе - не нажатой. Чтобы упростить проверку массива, в заголовочном файле определены мнемонические константы для всех клавиш клавиатуры, например DIK_ESCAPE соответствует клавише Escape. Вообще, массив значений предоставляет возможность одновременного получения информации о нескольких нажатых клавишах клавиатуры или даже обо всех клавишах сразу (если вы искусно прижмёте её большой подушкой :-) ).

Вот и все действия для работы в клавиатурой средствами DirectInput! Думаю, с удалением объектов вы разберётесь с самостоятельно

Mouse

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

Я не буду подробно описывать работу этого примера, остановлюсь лишь на самых важных деталях. Для получения буферизированных данных необходимо задать размер буфера. Для этого служит метод IDirectInputDevice8.SetProperty(). Первым параметром необходимо передать тип устанавливаемого свойства - воспользуемся константой DIPROP_BUFFERSIZE, тем самым указав, что мы желаем установить размер буфера для данных. Вторым параметром передаём адрес структуры TDIPROPDWORD. Как всегда, её нужно обнулить и заполнить поля diph.dwSize и diph.dwHeaderSize размерами структур TDIPROPDWORD и TDIPROPHEADER соответственно. Размер буфера задаётся в поле dwData.

Интересен механизм получения данных. Т. к. мы будем получать информацию из буфера, опрос необходимо организовать в виде цикла while to, где условием прекращения цикла будет отсутствие неизвлечённых данных из буфера. За один цикл из буфера извлекается один элемент (например, ось X, ось Y, клавиша 1 и т. д.). Опрос производится методомIDirectInputDevice8.GetDeviceData(). Этот метод используется только в случае работы с буфером. Первый параметр представляет собой размер структуры TDIDEVICEOBJECTDATA, адрес которой передаётся вторым параметром. Эта структура и позволит нам узнать о состоянии элемента мыши. Третий параметр - переменная типа DWORD, передаваемая по ссылке, после вызова метода содержит количество оставшихся в буфере неизвлечённых элементов. Когда все элементы извлечены, переменная равна 0. Именно её мы используем в качестве условия в цикле while to. Последнему параметру метода нужно передать 0.

Узнать, какой элемент извлечён из буфера, позволяет поле dwOfs структуры TDIDEVICEOBJECTDATA. Его значение меняется в каждом новом цикле и сравнивается с предопределёнными константами из файла DirectInput8.pas - например DIMOFS_BUTTON0 свидетельствует о том, что извлечён элемент данных, отвечающий за состояние первой кнопки мыши. Информация о состоянии того или иного объекта устройства вода содержится в поле dwData и тоже меняется с каждым новым циклом.

Заметьте, интерфейс функции UpdateMouseState() состоит из двух переменных типа DWORD, передаваемых по ссылке. В них записываются величины смещения координат мыши относительно прежних координат - например -5, +13. Получив данные о смещении, необходимо вычислить абсолютные координаты - делается это уже в процедуре TForm1.Idle().

А вот как получить данные от колёсика, я не знаю :-(

Mouse2

Этот пример демонстрирует работу с мышью путём получения непосредственных данных. Всё осталось на прежних местах, изменился лишь алгоритм опроса. Теперь мы снова используем метод IDirectInputDevice8.GetDeviceState(), передавая уже размер и адрес структуры TDIMOUSESTATE.

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

Я попытался реализовать похожий механизм в таком алгоритме:

if ms.lX < 0 then ms.lX := Round( ms.lX * CURSOR_SPEED ) else if ms.lX > 0 then ms.lX := Round( ms.lX * CURSOR_SPEED );if ms.lY < 0 then ms.lY := Round( ms.lY * CURSOR_SPEED ) else if ms.lY > 0 then ms.lY := Round( ms.lY * CURSOR_SPEED );

Алгоритм не универсален и не очень гибок, предлагаю вам усовершенствовать его. Если же эти строки закомментировать, то различий в работе между этим и предыдущим примером вы не увидите.

Joystick

Ну, все мы знаем, что такое джойстик и ему подобные игровые устройства управления. Главным образом они используются в играх для управления персонажем или например, самолётом. Есть такие игры, в которых управление с клавиатуры противопоказано (например, файтинги и авиасимуляторы). Поэтому часто игроманы покупают всякие штурвалы, джойстики, рули и геймпады для получения полного наслаждения от игры. Чтобы вы имели хоть какое-то представление о том, как выглядят современные игровые контроллеры, я собрал несколько фотографий с их изображением папке Show.

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

Реализовать опрос игровых контроллеров ненамного сложнее, чем например, опрос клавиатуры. Основным отличием является то, что сперва контроллер надо найти, а потом уже получать от него данные - ведь он не обязательный атрибут ПК. Откройте проект и найдите функцию InitDirectInput(). Сперва мы создаём главный объект DirecInput8 - если вы изучали примеры последовательно, то для вас уже должно быть всё понятно. Затем нужно методом IDirectInput8.EnumDevices() перечислить установленные устройства ввода. Первый параметр - константа, определяющая, какие устройства нужно перечислить. При указании DI8DEVCLASS_GAMECTRL будут перечислены только устройства, относящиеся к классу игровых контроллеров. Указав, например, DI8DEVCLASS_ALL, добъёмся перечисления ВСЕХ устройств ввода. Второй параметр представляет собой адрес функции обратного вызова, которая будет вызываться для для каждого найденного в системе устройства. Формат функции будет рассмотрен ниже. Третий параметр pvRef позволяет передать в функцию какие-то данные, например, структуру или строку. В SDK 8 в одном из примеров через неё передаётся дескриптор главного окна. Последний флаг позволяет фильтровать перечисляемые устройства. В данном случае необходимо указать константу DIEDFL_ATTACHEDONLY, что позволит перечислить только подключенные устройства ввода. Константа DIEDFL_ALLDEVICES позволит найти все устройства, для которых установлены соответствующие драйверы, даже если получить данные от них будет невозможно.

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

TDIEnumDevicesCallbackA = function (var lpddi : TDIDeviceInstanceA; pvRef : Pointer) : Integer; stdcall;

Наибольший интерес представляет структура TDIDEVICEINSTANCE. Она содержит разнообразную информацию об устройстве ввода. Как только будет найдено первое, метод IDirectInput8.EnumDevices() вызовет указанную callback-функцию и эта структура будет инициализирована соответствующим образом. Т. к. для простоты примера используется первое найденное устройство, то тут же и создадим объект для работы с ним методом IDirectInputDevice8.CreateDevice(). В данном случае необходимо указать конкретный GUID устройства - он находится в поле guidInstance структуры TDIDEVICEINSTANCE. Для наглядности я осуществил определение типа найденного устройства - это делается макросом GET_DIDEVICE_TYPE() - единственным параметром нужно указать поле dwDevType давешней структуры TDIDEVICEINSTANCE. Ещё определяется количество кнопок и осей устройства - думаю, с этим в разберётесь самостоятельно.

Если нас устроил найденный контроллер, функция обратного вызова должна вернуть DIENUM_STOP, иначе - DIENUM_CONTINUE, чтобы продолжить перечисление.

Вернёмся в функцию InitDirectInput() - следующими шагами будет установка формата данных и уровня кооперации. Заметьте, что в случае с игровым контроллером можно вести себя подобно хищнику - свободно устанавливаются флаги DISCL_BACKGROUND и DISCL_EXCLUSIVE.

Следующим действием необходимо указать область диапазона координат, в которых будут представляться данные от осей. Дело в том, что крайние значения положения рукоятки штурвала или Pad-a геймпада могут представляться разными значениями. Если явно не указать этот интервал, то, например, крайнее левое положение рукоятки будет представлено значением 65535, а крайнее правое - 0. Центральное положение будет представлено значением 32768. Это не всегда удобно, хотя для минимизации кодирования можно и не задавать интервал. Если же вы решите сделать это, то есть два способа - перечислить оси и для каждой найденной установить интервал, или самостоятельно установить инервал для каждой оси. В SDK 8 представлен пример только с перечислением осей. Поступим так и мы.

Перечисление каких-либо элементов устройства подобно перечислению самих устройств. Производится оно методом IDirectInput8.EnumObject(). Первым параметром необходимо передать функцию обратного вызова, второй служит для передачи каких-либо данных в эту функцию, третий отвечает за тип перечисляемых объектов. Константа DIDFT_AXIS позволит перечислить все оси, DIDFT_BUTTON - кнопки всех типов и т. д.

Для изменение какого-либо свойства составного элемента предназначен метод IDirectinputDevice8.SetProperty(). Первый параметр - тип устанавливаемого свойства. Константа DIPROP_RANGE предназначена для установки области диапазона координат. Втрой параметр - поле diph структуры TDIPROPRANGE. Именно эта структура служит для задания области координат. Думаю, вы разберётесь с её инициализацией сами с помощью Help'а SDK. Замечу, что если поля lMin и lMax имеют по модулю одинаковое значение, то центральное положение рукоятки (и ей подобных элементов) будет представлено значением 0. Поле lMin не может быть больше lMax.

После этого приводится в соответствие GUI в окне - делаются доступными только те метки, для которых обнаружена соответствующая ось или ползунок. Узнать, какая ось перечислена, можно, сравнив поле guidType структуры TDIDeviceObjectInstanceA с одним из предопределённых значений. К сожалению, текущая реализация языка Pascal не позволяет сравнить обе структуры за один "присест" в выражении вида

if lpddoi.guidType = GUID_XAxis then...

поэтому пришлось прибегнуть к отдельной процедуре, сравнивающей оба GUID поэлементно. Хорошо ещё, что не пришлось сравнивать структуры, например, из 256 элементов.

Осталось только "захватить" устройство и начать получать от него данные - делается это методом IDirectInputDevice8.GetDeviceData(). Только теперь используется структура TDIJOYSTATE. Обратите внимание, что дополнительно нужно вызвать метод IDirectInputDevice8.Poll(). Это необходимо сделать для большинства устройств, но не для всех.

Joystick2

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

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

Обычно у многих джойстиков возникают паразитные колебания в тот момент, когда рукоятка находится в центральном положении и вы даже не касаетесь её рукой. К тому же, часто для поворота персонажа в игре приходится отклонять ручку управления в крайние положения с большим давлением, что может привести к быстрому износу контактов. Чтобы избежать этих неудобств, применяются так называемые области покоя (deadzone) и насыщения (saturation).

Область покоя для оси - это область координат, в пределах которой положение рукоятки воспринимается как центральное. Даже если вы отклонили её, но текущие координаты находятся в области покоя, DirectInput будет считать, что

– Конец работы –

Эта тема принадлежит разделу:

Состоит из следующих компонентов

Введение... Фанаты игр часто встречаются с аббревиатурой quot DirectX quot На упаковках... Компоненты DirectX обеспечивают не только прямой доступ к устройствам компьютера они избавляют программиста от...

Если Вам нужно дополнительный материал на эту тему, или Вы не нашли то, что искали, рекомендуем воспользоваться поиском по нашей базе работ: Почему я не рекомендую использовать DelphiX

Что будем делать с полученным материалом:

Если этот материал оказался полезным ля Вас, Вы можете сохранить его на свою страничку в социальных сетях:

Все темы данного раздела:

DirectShow
используется в мультимедиа-технологиях - можно выполнить высококачественое воспроизведение или захват видео и звука. Распространённость использования компоненты не слишком велика Все компо

Виктор Кода
Смотрите по теме : DirectX для начинающих.Часть вторая DirectX для начинающих. Часть третья. Считывание и запись Цикл лекция JINX'а Direc

Небольшое отступление
Прошёл месяц с тех пор как я написал первую часть ( http://www.delphikingdom.com/helloworld/directx.htm ) статьи по использованию DirectX в среде Delphi. У меня накопилось ещё несколько примеров, к

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

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

E_string.pas
- две функции - ltos() и ltoc() для преобразования типа longint к строке string или pchar соответственно. Базируются на процедуре str() из модуля system.pas. Это здорово сокращает объём исполняемог

Несколько слов о недоработках.
Первое: пример MainExample в окне в видеорежимах HighColor или TrueColor на всех компьютерах с видеокартами GeForce2 MX 400, где я его тестировал, почему-то работает некорректно. Н

Хотите получать на электронную почту самые свежие новости?
Education Insider Sample
Подпишитесь на Нашу рассылку
Наша политика приватности обеспечивает 100% безопасность и анонимность Ваших E-Mail
Реклама
Соответствующий теме материал
  • Похожее
  • Популярное
  • Облако тегов
  • Здесь
  • Временно
  • Пусто
Теги