netlib.narod.ru | < Назад | Оглавление | Далее > |
То, что вы прочитали можно назвать самым коротким обзором DirectInput. Причина подобной краткости в том, что стратегические игры не требуют сложных устройств ввода. Нет никакой необходимости использовать устройства с обратной связью, джойстики, игровые пульты и другие подобные устройства. В стратегических играх непревзойденными остаются старые добрые клавиатура и мышь.
В сопроводительные файлы книги включен проект DInput_Simple. Он строит небольшое приложение, создающее объект клавиатуры и читающее поступающие от него данные. Окно программы показано на рис. 9.2.
Рис. 9.2. Окно программы DInput_Simple
На рис. 9.2 изображено обычное окно с текстом, сообщающим, что для выхода из программы следует нажать на клавишу Esc. Вместо того, чтобы использовать для перехвата нажатия на клавишу Esc сообщения Windows, мы воспользуемся DirectInput и устройством клавиатуры. Теперь загрузите проект и следуйте за мной далее.
Проект содержит два файла: main.cpp и main.h. В файле main.cpp находится код реализации функций, а в заголовочном файле main.h сосредоточена вся заголовочная информация. Проекту необходимы две библиотеки: dxguid.lib и dinput8.lib. Библиотека dxguid.lib содержит уникальные GUID для устройств DirectInput. В библиотеке dinput8.lib находятся сами функции DirectInput.
Откройте файл main.cpp и найдите код функции WinMain(). В ней вы найдете обычный код создания объектов Windows, за которым следует код инициализации DirectInput и устройства клавиатуры, выглядящий так:
// Инициализация DirectInput iResult = iInitDirectInput(); if(iResult != INPUTERROR_SUCCESS) { MessageBox(hWnd, "DirectInput Error", "Unable to initialize Direct Input.", MB_ICONERROR); vCleanup(); exit(1); } // Инициализация клавиатуры DI iResult = iInitKeyboard(hWnd); if(iResult != INPUTERROR_SUCCESS) { MessageBox(hWnd, "DirectInput Error", "Unable to initialize Keyboard.", MB_ICONERROR); vCleanup(); exit(1); }
В приведенном выше коде вызываются две функции: iInitDirectInput() и iInitKeyboard(). Вызов первой из них инициализирует главный объект DirectInput, а вызов второй создает устройство клавиатуры. Увидеть ход выполнения программы можно на рис. 9.3.
Рис. 9.3. Ход выполнения программы DInput_Simple
Функция iInitDirectInput() — это мое собственное творение и я использую ее для создания главного объекта DirectInput. Код, используемый мной для создания упомянутого объекта должен выглядеть для вас очень знакомым, поскольку я уже описывал его в предыдущем разделе главы. Здесь я привожу полный код функции:
int iInitDirectInput(void) { HRESULT hReturn; // Не пытаться создать Direct Input, если он уже создан if(!pDI) { // Создаем объект DInput if(FAILED(hReturn = DirectInput8Create( g_hInstance, DIRECTINPUT_VERSION, IID_IDirectInput8, (VOID**)&pDI, NULL))) { return(INPUTERROR_NODI); } } else { return(INPUTERROR_DI_EXISTS); } return(INPUTERROR_SUCCESS); }
В приведенном выше коде я сперва проверяю, существует ли объект DirectInput. Если да, мне не надо создавать еще один объект. В этом случае функция возвращает код ошибки, говорящий вызывающей программе, что объект уже создан.
В следующем блоке кода выполняется вызов функции DirectInput8Create() для создания объекта DirectInput. Как только он будет успешно выполнен, моя функция возвращает WinMain() код успешного завершения. В результате этих действий глобальный указатель pDI будет содержать ссылку на созданный при вызове функции объект DirectInput.
Теперь, когда у нас есть действующий объект ввода в форме глобального указателя pDI, можно создать интерфейс объекта клавиатуры. Здесь выходит на сцену моя функция iInitKeyboard(). В ней я создаю устройство клавиатуры, устанавливаю буфер клавиатуры, задаю режим доступа, захватываю клавиатуру и получаю раскладку клавиатуры. Вот как выглядит код функции:
int iInitKeyboard(HWND hWnd) { HRESULT hReturn = 0; DIPROPDWORD dipdw; // Не пытайтесь создать клавиатуру дважды if(pKeyboard) { return(INPUTERROR_KEYBOARDEXISTS); } // Выход, если не найден интерфейс DirectInput else if (!pDI) { return(INPUTERROR_NODI); } // Получаем интерфейс устройства системной клавиатуры if(FAILED(hReturn = pDI->CreateDevice( GUID_SysKeyboard, &pKeyboard, NULL))) { return(INPUTERROR_NOKEYBOARD); } // Создаем буфер для хранения данных клавиатуры ZeroMemory(&dipdw, sizeof(DIPROPDWORD)); dipdw.diph.dwSize = sizeof(DIPROPDWORD); dipdw.diph.dwHeaderSize = sizeof(DIPROPHEADER); dipdw.diph.dwObj = 0; dipdw.diph.dwHow = DIPH_DEVICE; dipdw.dwData = KEYBOARD_BUFFERSIZE; // Устанавливаем размер буфера if(FAILED(hReturn = pKeyboard->SetProperty( DIPROP_BUFFERSIZE, &dipdw.diph))) { return(INPUTERROR_NOKEYBOARD); } // Устанавливаем формат данных клавиатуры if(FAILED(hReturn = pKeyboard->SetDataFormat( &c_dfDIKeyboard))) { return(INPUTERROR_NOKEYBOARD); } // Устанавливаем уровень кооперации для монопольного доступа if(FAILED(hReturn = pKeyboard->SetCooperativeLevel( hWnd, DISCL_NONEXCLUSIVE | DISCL_FOREGROUND ))) { return(INPUTERROR_NOKEYBOARD); } // Захватываем устройство клавиатуры pKeyboard->Acquire(); // Получаем раскладку клавиатуры g_Layout = GetKeyboardLayout(0); return(INPUTERROR_SUCCESS); }
Гм-м — многовато кода для простой инициализации клавиатуры, не так ли? В действительности все не так уж и плохо, если учесть чего мы с помощью этого кода достигнем.
Первая часть кода проверяет не проинициализирован ли уже указатель pKeyboard. Если да, объект клавиатуры уже создан ранее и функция возвращает код ошибки, извещающий нас об этом. В следующей проверке мы убеждаемся, что существует объект ввода pDI. Если инициализация DirectInput не выполнена, нет смысла пытаться создать объект клавиатуры!
Как только необходимые проверки успешно пройдены, я вызываю функцию CreateDevice() для создания устройства клавиатуры. Ранее я уже описывал эту функцию, так что код должен выглядеть для вас очень знакомо.
Следующая часть функции может показаться вам странной, поскольку пока я еще не объяснил ее назначение. Дело в том, что для клавиатуры имеется два способа получения входных данных: непосредственный и буферизованный. Непосредственный ввод позволяет получить состояние клавиш на момент опроса. Если пользователь нажал клавишу хотя бы на 1/100 секунды раньше, это событие будет пропущено, поскольку оно не произошло именно в тот момент, когда выполнялась проверка. В игре это представляет серьезную проблему, поскольку циклы визуализации и обработки данных отнимают много времени, что может привести к частой потере вводимых данных. Данный момент проиллюстрирован на рис. 9.4.
Рис. 9.4. Непосредственное чтение данных клавиатуры
На рис. 9.4 видно, что программа обработала только нажатие клавиши L, поскольку возвращаются только данные о непосредственно нажатых клавишах.
Вы когда-нибудь играли в игру, которая в половине случаев игнорирует нажатия на клавиши? Наиболее часто нажатия клавиш теряются когда процессор загружен выводом графики или какими-нибудь другими задачами. Причина пропуска изменений состояний клавиш заключается в том, что программа не использует буферизованный ввод, который позволяет системе обработать каждое изменение состояний клавиш, произошедшее с момента последнего опроса устройства. Буферизованный ввод показан на рис. 9.5.
Рис. 9.5. Буферизованный ввод с клавиатуры
На рис. 9.5 показан тот же процесс, что и на рис. 9.4, за исключением того, что функция чтения с клавиатуры получает каждое нажатие клавиш, произошедшее с начала игрового цикла. Это более мощный метод, чем непосредственный захват, и я предлагаю вам всегда использовать его.
Реализация буферизованного ввода достаточно проста — достаточно установить свойство устройства клавиатуры. Это осуществляется с помощью функции установки свойств. Вот как выглядит ее прототип:
HRESULT SetProperty( REFGUID rguidProp, LPCDIPROPHEADER pdiph );
Первый параметр, rguidProp, является GUID того свойства устройства, которое вы хотите установить. Чтобы установить размер буфера устройства используйте значение DIPROP_BUFFERSIZE.
Второй параметр, pdiph, является структурой данных, содержащей информацию о создаваемом буфере. Тип этой структуры данных — DIPROPDWORD. В коде я заполняю эту структуру данных нулями и устанавливаю параметр, определяющий размер создаваемого буфера клавиатуры. Количество сохраняемых в буфере событий клавиатуры задает следующая строка кода:
dipdw.dwData = KEYBOARD_BUFFERSIZE;
Поле dwData определяет максимальное количество сохраняемых в буфере событий клавиатуры. В рассматриваемом примере я использую значение 10. Вы можете поиграться с этой константой, чтобы подобрать более подходящее для вашей игры значение.
Затем вы должны задать формат данных клавиатуры. Это простая формальность, для соблюдения которой достаточно вызвать функцию IDirectInputDevice8::SetDataFormat(). Функция получает один параметр, задающий формат данных устройства. Для клавиатуры используйте значение c_dfDIKeyboard. Если же вам необходимо задать формат данных для мыши, воспользуйтесь значением c_dfDIMouse.
Поскольку DirectX предоставляет прямой доступ к аппаратуре, очень важен уровень кооперации устройства. Он определяет как программа может использовать данный ресурс совместно с другими приложениями. Если вы установите монопольный режим, больше никто не сможет воспользоваться данным ресурсом. Если вы установите совместный режим, то доступ к клавиатуре смогут получить все желающие. Уверен, вы можете вспомнить игры, которые не делят клавиатуру ни с кем. Мне на ум приходит EverQuest. Поскольку создатели игры не хотели, чтобы сторонние разработчики писали приложения для их игры, они заблокировали использование клавиатуры вне их программы. Это не слишком хорошо и может вызвать настоящие проблемы, если вы переключитесь из игры на другое приложение, чтобы проверить почту или сделать что-нибудь еще.
Для установки уровня кооперации применяется функция IDirectInputDevice8::SetCooperativeLevel(). Вот ее прототип:
HRESULT SetCooperativeLevel( HWND hwnd, DWORD dwFlags );
В ее первом параметре, hwnd, передается дескриптор окна, которое будет связано с устройством. Я в этом параметре передаю дескриптор, который был возвращен мне при создании главного окна.
Второй параметр, dwFlags, задает уровень кооперации устройства. Доступные уровни перечислены в таблице 9.1.
Таблица 9.1. Уровни кооперации устройств | |
Значение | Описание |
DISCL_BACKGROUND | Доступ к клавиатуре будет предоставлен даже если окно свернуто. |
DISCL_EXCLUSIVE | Предоставляется монопольный доступ к клавиатуре, для всех остальных клавиатура недоступна. |
DISCL_FOREGROUND | Доступ к данным клавиатуры предоставляется только когда окно активно. |
DISCL_NONEXCLUSIVE | Устройство используется совместно с другими программами. |
DISCL_NOWINKEY | Блокирует клавишу Windows. |
Для рассматриваемого примера я устанавливаю флаги уровня кооперации DISCL_NONEXCLUSIVE и DISCL_FOREGROUND. Благодаря этому программа использует клавиатуру совместно с другими приложениями, а сама может читать данные клавиатуры только когда ее окно активно.
Последний, относящийся к DirectX этап — вызов функции IDirectInputDevice8::Acquire(). Эта функция необходима для привязки приложения к используемому им устройству ввода. Всякий раз когда окно теряет фокус клавиатура должна быть захвачена снова.
В рассматриваемом примере я покажу вам как считывать коды клавиш DirectInput и ASCII-коды клавиш. Чтобы получить возможность преобразования кодов DIK в коды ASCII вы должны вызвать функцию GetKeyboardLayout(). Она получает раскладку подключенной к системе клавиатуры для дальнейшего использования.
Этапы, необходимые для инициализации клавиатуры, показаны на рис. 9.6.
Рис. 9.6. Этапы инициализации клавиатуры
Вернемся к функции WinMain() и рассмотрим следующий фрагмент кода:
while(msg.message != WM_QUIT) { if(PeekMessage(&msg, NULL, 0U, 0U, PM_REMOVE)) { TranslateMessage(&msg); DispatchMessage(&msg); } else { // Чтение из буфера клавиатуры iResult = iReadKeyboard(); // Проверяем, сколько нажатий на клавиши возвращено if(iResult) { // Цикл обработки полученных данных for(i = 0; i < iResult; i++) { // Выход из программы, если нажата клавиша ESC if(diks[DIK_ESCAPE][i]) { PostQuitMessage(0); } else if (ascKeys[13][i]) { PostQuitMessage(0); } } } } }
Представленный код является стандартным циклом обработки сообщений Windows. Его ключевой особенностью является вызов функции iReadKeyboard(). Обращение к ней происходит каждый раз, когда в очереди нет системных сообщений для обработки. Функция возвращает количество зафиксированных изменений состояний клавиш и сохраняет их в глобальных массивах diks и ascKeys. Если функция возвратила какие-нибудь данные, программа в цикле перебирает полученные изменения состояний клавиш и проверяет не была ли нажата клавиша Esc. Если клавиша была нажата, выполнение программы завершается.
Вместо того, чтобы одним махом показать вам весь код функции, я разделил его на небольшие кусочки. Вот первый фрагмент функции iReadKeyboard():
if(!pKeyboard || !pDI) { return(INPUTERROR_NOKEYBOARD); }
Этот маленький фрагмент кода проверяет существуют ли объекты клавиатуры и DirectInput. Если какого-нибудь из них нет, функция возвращает код ошибки. Пришло время следующего фрагмента:
hr = pKeyboard->GetDeviceData( sizeof(DIDEVICEOBJECTDATA), didKeyboardBuffer, &dwItems, 0);
Вызов функции получения данных от устройства возвращает любые данные, находящиеся в буфере устройства ввода. В данном случае возвращается буфер клавиатуры. Переменная dwItems будет содержать количество возвращенных элементов, а сами они будут помещены в буфер didKeyboardBuffer. Переменная hr сохраняет код завершения, возвращаемый функцией получения данных от устройства. Логика проверки кода завершения выглядит следующим образом:
// Клавиатуа может быть потеряна, захватить устройство снова if(FAILED(hr)) { pKeyboard->Acquire(); return(INPUTERROR_SUCCESS); }
Если переменная hr содержит код ошибки, это может быть вызвано тем, что клавиатура потеряна из-за сворачивания окна или каких-нибудь других действий. В этом случае нужно повторно захватить клавиатуру с помощью функции захвата устройства.
Если мы без ошибок прошли все предыдущие этапы, настало время в цикле получить данные от устройства и заполнить ими глобальный буфер клавиатуры. Соответствующий код представлен ниже:
// Если есть данные, обработаем их if (dwItems) { // Обработка данных for(dwCurBuffer = 0; dwCurBuffer < dwItems; dwCurBuffer++) { // Преобразование скан-кода в код ASCII byteASCII = Scan2Ascii( didKeyboardBuffer[dwCurBuffer].dwOfs); // Указываем, что клавиша нажата if(didKeyboardBuffer[dwCurBuffer].dwData & 0x80) { ascKeys[byteASCII][dwCurBuffer] = 1; diks[didKeyboardBuffer[dwCurBuffer].dwOfs] [dwCurBuffer] = 1; } // Указываем, что клавиша отпущена else { ascKeys[byteASCII][dwCurBuffer] = 0; diks[didKeyboardBuffer[dwCurBuffer].dwOfs] [dwCurBuffer] = 0; } } }
Код проверяет были ли возвращены какие-нибудь данные функцией получения данных от устройства. Если да, код в цикле перебирает возвращенные элементы в буфере и сохраняет результаты в глобальных массивах diks и ascKeys.
Массив didKeyboardBuffer хранит данные возвращенные DirectInput. Чтобы сделать их читаемыми, необходимо проверить значение каждого элемента массива. Если результат поразрядной логической операции И над возвращенным значением и константой 0x80 не равен нулю, значит клавиша была нажата; в ином случае клавиша была отпущена. Я знаю, это выглядит причудливо, но именно так работает DirectInput!
Для преобразования кодов DIK в коды ASCII я написал следующую функцию:
BYTE Scan2Ascii(DWORD scancode) { UINT vk; // Преобразование скан-кода в код ASCII vk = MapVirtualKeyEx(scancode, 1, g_Layout); // Возвращаем код ASCII return(vk); }
Функция получает код клавиши DirectInput и вызывает функцию MapVirtualKeyEx() для преобразования его в ASCII. Для работы функции отображения кодов необходимы данные о раскладке клавиатуры, которые мы получили на этапе инициализации.
netlib.narod.ru | < Назад | Оглавление | Далее > |