1. 程式人生 > >【Visual C++】遊戲開發筆記四十二 淺墨DirectX教程之十 遊戲輸入控制利器:DirectInput專場

【Visual C++】遊戲開發筆記四十二 淺墨DirectX教程之十 遊戲輸入控制利器:DirectInput專場

本系列文章由zhmxy555(毛星雲)編寫,轉載請註明出處。  

------------------------------------------------------------------------------------------------------------------------------

淺墨歷時一年為遊戲程式設計愛好者鍛造的著作《逐夢旅程:Windows遊戲程式設計之從零開始》如果你喜歡淺墨寫的【Visual C++】遊戲開發系列部落格文章,那麼你一定會愛上這本書。這是淺墨專門為熱愛遊戲程式設計的朋友們寫的入門級遊戲程式設計寶典。噹噹網|京東商城|亞馬遜

------------------------------------------------------------------------------------------------------------------------------

 

在本篇文章中,我們一起詳細探索了DirectInput這套在PC遊戲即時控制方面一手遮天的API。下面先來看一下這篇一萬多字文章的大體脈絡。首先我們對DirectInput介面進行了整體上的講解,然後深入DirectInput的使用步驟進行具體的探索,最後淺出,歸納出DirectInput使用五步曲,方便大家的快速掌握。文章最後,我們配了一個比較好玩的demo來讓大家對本篇文章所學的DirectInput的使用融會貫通,最後提供了這個demo詳細註釋的原始碼下載。先放一個demo的截圖吊吊大家胃口,哈哈:


一、引言

眾所周知,在普通的Windows程式中,使用者通過鍵盤或者滑鼠輸入的訊息並不是應用程式直接處理的,而是通過Windows的訊息機制轉發給Windows作業系統的。Windows作業系統對這些訊息進行響應後,在通過回撥應用程式的視窗過程函式進行相應的訊息處理。

這顯然滿足不了對於效能要求比較苛刻的遊戲程式的。在DirectX中,微軟為我們提供了名為DirectInput介面物件來實現使用者輸入的。DirectInput直接和硬體驅動打交道,因此處理起使用者的輸入來說非常迅速。

首先需要給大家說明的是,DirectInput這套API自DirectX8更新以來,功能已經足夠完善了。所以儘管當前DirectX的最新版本上升到了DirectX 11,DirectInput還是DirectX 8那個版本時代的老樣子,API的內容和功能隨著最近幾個版本的更迭卻原封不動,名稱上也保留了8這個版本號,依然叫DirectInput 8,可謂以不變應萬變。即目前最新版本的DirectInput ,依舊是

DirectInput 8。

二、DirectInput介面概述

DirectInput作為DirectX的元件之一,自然依然是一些COM物件的集合。DirectInput由IDirectInput8、IDirectInputDevice8,IDirectInputEffect這三個介面組成,這三個介面中又分別含有各自的方法。

總的來說,當前版本的DirectInputAPI中,三個介面,四十七個方法,組成了這個在電腦遊戲開發中不可或缺的元件。

由於IDirectInput8 API整體來說規模不大,說白了也就是三個介面,四十七個方法,不妨我們在文章中將他們一一列舉出來,也讓大家窺一窺DirectInput API的全貌。

1.IDirectInput8介面 函式一覽

IDirectInput8::ConfigureDevices Method

IDirectInput8::CreateDevice Method

IDirectInput8::EnumDevices Method

IDirectInput8::EnumDevicesBySemantics Method

IDirectInput8::FindDevice Method

IDirectInput8::GetDeviceStatus Method

IDirectInput8::Initialize Method

IDirectInput8::RunControlPanel Method

2.IDirectInputDevice8介面 函式一覽

IDirectInputDevice8::Acquire Method

IDirectInputDevice8::BuildActionMap Method

IDirectInputDevice8::CreateEffect Method

IDirectInputDevice8::EnumCreatedEffectObjects Method

IDirectInputDevice8::EnumEffects Method

IDirectInputDevice8::EnumEffectsInFile Method

IDirectInputDevice8::EnumObjects Method

IDirectInputDevice8::Escape Method

IDirectInputDevice8::GetCapabilities Method

IDirectInputDevice8::GetDeviceData Method

IDirectInputDevice8::GetDeviceInfo Method

IDirectInputDevice8::GetDeviceState Method

IDirectInputDevice8::GetEffectInfo Method

IDirectInputDevice8::GetForceFeedbackState Method

IDirectInputDevice8::GetImageInfo Method

IDirectInputDevice8::GetObjectInfo Method

IDirectInputDevice8::GetProperty Method

IDirectInputDevice8::Initialize Method

IDirectInputDevice8::Poll Method

IDirectInputDevice8::RunControlPanel Method

IDirectInputDevice8::SendDeviceData Method

IDirectInputDevice8::SendForceFeedbackCommand Method

IDirectInputDevice8::SetActionMap Method

IDirectInputDevice8::SetCooperativeLevel Method

IDirectInputDevice8::SetDataFormat Method

IDirectInputDevice8::SetEventNotification Method

IDirectInputDevice8::SetProperty Method

IDirectInputDevice8::Unacquire Method

IDirectInputDevice8::WriteEffectToFile Method

3.IDirectInputEffect介面 函式一覽

IDirectInputEffect::Download Method

IDirectInputEffect::Escape Method

IDirectInputEffect::GetEffectGuid Method

IDirectInputEffect::GetEffectStatus Method

IDirectInputEffect::GetParameters Method

IDirectInputEffect::Initialize Method

IDirectInputEffect::SetParameters Method

IDirectInputEffect::Start Method

IDirectInputEffect::Stop Method

IDirectInputEffect::Unload Method

其中,IDirectInput8作為DirectInput API中最主要的介面,用於初始化系統以及建立輸入裝置介面,DirectInput中其他的所有的介面都需要依賴於我們的IDirectInput8之上,都是通過這個介面進行查詢的。而DirectInputDevice8介面用於表示各種輸入裝置(如鍵盤、滑鼠和遊戲杆),並提供了相同的訪問和控制方法。對於某些輸入裝置(如遊戲杆和滑鼠),都能通過查詢各自的IDirectInputDevice8介面物件,得到另一個介面IDirectInputEffect8。而IDirectInputEffect8介面則用於控制裝置的力反饋效果。

 

三、DirectInput使用步驟詳解

1.標頭檔案和庫檔案的包含

我們首先需要注意的是,在使用DirectInput時,需要保證我們包含了DInput.h標頭檔案,並且在專案中已經連結了DInput8.lib庫檔案。

當然,庫檔案我們也可以動態新增:

#pragma comment(lib, "dinput8.lib")     // 使用DirectInput必須包含的標頭檔案,注意這裡有8

2.建立DirectInput介面和裝置

在DirectInput中我通過們呼叫DirectInputCreate函式建立並初始化IDirectInput介面,我們可以在MSDN中查到該函式的宣告如下:

HRESULT DirectInput8Create(
         HINSTANCE hinst,
         DWORD dwVersion,
         REFIID riidltf,
         LPVOID * ppvOut,
         LPUNKNOWN punkOuter
)

■ 第一個引數,HINSTANCE型別的hinst,表示我們當前建立的DirectInput的Windows程式控制代碼,這個值填我們在WinMain函式的引數中的例項控制代碼就可以了。

■ 第二個引數,DWORD型別的dwVersion,表示我們當前使用的DirectInput版本號,通常可以取DIRECTINPUT_VERSION或者DIRECTINPUT_HEADER_VERSION,這兩個值對應的是同一個值,為0x0800。所以我們在這裡還可以直接填0x0800。

歸根揭底的話,可以通過【轉到定義】大法在dinput.h中查到有如下程式碼:

#define DIRECTINPUT_HEADER_VERSION 0x0800
#ifndef DIRECTINPUT_VERSION
#define DIRECTINPUT_VERSION       DIRECTINPUT_HEADER_VERSION

大體意思很清楚了吧,先定義一下DIRECTINPUT_HEADER_VERSION=0x0800,然後再說如果沒有定義DIRECTINPUT_VERSION的話,就定義一個DIRECTINPUT_VERSION= DIRECTINPUT_HEADER_VERSION。

■ 第三個引數,REFIID型別的riidltf,表示介面的標誌,通常取IID_IDirectInput8就可以了。

■ 第四個引數,LPVOID 型別的* ppvOut,用於返回我們新建立的IDirectInput8介面物件的指標。

■ 第五個引數,LPUNKNOWN型別的punkOuter,一個和COM物件介面相關的引數,通常我們設為NULL就可以了。

這個函式執行成功的話TINPUTVER會返回HRESULT型別的DI_OK,而失敗的話根據不同的呼叫失敗原因,會返回DIERR_BETADIRECSION,DIERR_INVALIDPARAM,DIERR_OLDDIRECTINPUTVERSION, DIERR_OUTOFMEMORY中的一個。所以我們可以根據FAILED巨集來判斷我們IDirectInput8介面物件是否建立成功了。

下面是一個呼叫的例子:

// 建立DirectInput裝置
LPDIRECTINPUT8         g_pDirectInput      = NULL;
if(FAILED(DirectInput8Create(hInstance,0x0800, IID_IDirectInput8,(void**)&g_pDirectInput, NULL)))
              return E_FAIL;

這步完成之後,咱們的定義的DIRECTINPUT8介面物件g_pDirectInput就有了權利,新官上任了。

在IDirectInput8介面中包含了很多用於初始化輸入裝置及獲得裝置介面的方法。其中,常用的方法為EnumDevices和CreateDevices。前者EnumDevices用於獲得輸入裝置的型別,而後者CreateDevices用於為輸入裝置建立IDirectInputDevice8介面物件。

系統中每一個已安裝的裝置都有一個系統分配的全域性唯一標示符(GUID,Global Unique Identification),從英文單詞意義上就可以知道,系統中的每個裝置都有著獨一無二的GUID,這個GUID又唯一的標誌了系統中的某某裝置。就像我們每個人都有著獨一無二的的身份證號碼。

要使用某個裝置的話,首先我們就需要知道他的GUID。

滑鼠和鍵盤作為我們電腦中最為重要的外設,DirectInput對他們做了特殊對待,給了後門,定義了他們的GUID分別為GUID_Keyboard和GUID_SysMouse。而對於其他的輸入裝置,我們就用上面提到過的EnumDevices方法枚舉出這些裝置,以得到他們的GUID,我們可以在MSDN中查到這個方法有如下宣告:

HRESULTEnumDevices(
         DWORD dwDevType,
         LPDIENUMDEVICESCALLBACKlpCallback,
         LPVOID pvRef,
         DWORD dwFlags
)

■ 第一個引數,DWORD型別的dwDevType,指定我們需要列舉的裝置型別。

可取的值為DI8DEVCLASS_ALL,DI8DEVCLASS_DEVICE,DI8DEVCLASS_GAMECTRL,DI8DEVCLASS_KEYBOARD,DI8DEVCLASS_POINTER中的一個。

■ 第二個引數,LPDIENUMDEVICESCALLBACK型別的lpCallback,用於指定一個回撥函式的地址,當系統中每找到一個匹配的裝置時,就會自動呼叫這個回撥函式。

■ 第三個引數,LPVOID型別的pvRef,返回我們當前匹配裝置的GUID值。

■ 第四個引數,DWORD型別的dwFlags,指定我們列舉裝置的方式。取值可以下面的一個或者多個值:DIEDFL_ALLDEVICES,DIEDFL_ATTACHEDONLY,DIEDFL_FORCEFEEDBACK,DIEDFL_INCLUDEALIASES,DIEDFL_INCLUDEHIDDEN,DIEDFL_INCLUDEPHANTOMS。

取得我們需要使用的裝置的GUID後,就可以根據這個GUID來呼叫IDirectInput8介面的CreateDevice方法,進而來建立裝置的IDirectInputDevice8介面物件了。

我們可以在MSDN中查到IDirectInput8::CreateDevice方法的宣告如下:

HRESULTCreateDevice(
         REFGUID rguid,
         LPDIRECTINPUTDEVICE*lplpDirectInputDevice,
         LPUNKNOWN pUnkOuter
)

■ 第一個引數,REFGUID型別的rguid,就是填我們上面講到的輸出裝置的GUID。系統中當前使用的鍵盤對應GUID_SysKeyboard,當前使用的滑鼠對應GUID_SysMouse。其他裝置的話,就用我們剛剛講過的EnumDevices獲取一下就行了。

■ 第二個引數,LPDIRECTINPUTDEVICE型別的*lplpDirectInputDevice,表示我們所建立的輸入裝置物件的指標地址,可以說呼叫這個CreateDevice引數就是在初始化這個引數。

■ 第三個引數,LPUNKNOWN型別的pUnkOuter,、和COM物件的IUnknown介面相關的一個引數,一般我們不去管它,設為NULL就可以了。

講解完了,當然得看一個呼叫例項。下面的程式碼中CreateDevice方法的第二個引數我們填的是GUID_SysMouse,所以我們在為系統滑鼠建立一個DirectInput裝置介面物件:

LPDIRECTINPUTDEVICE8   g_pMouseDevice      = NULL;
if(FAILED (g_pDirectInput->CreateDevice(GUID_SysKeyboard,&g_pKeyboardDevice,NULL)))
              return E_FAIL;

3.設定資料格式

資料格式用於表示裝置狀態資訊的儲存方式,每種裝置都有一種用於讀取對應資料的特定資料格式,所以對每種裝置都要區別對待。所以要使程式從裝置讀入資料的話,首先我們需要告訴DirectInput讀取這種資料所採用的格式。

設定資料格式通常我們都是通過IDirectInputDevice8介面的SetDataFormat方法來做到的,這個方法可以把裝置的資料格式填充到一個DIDATAFORMAT介面型別的物件。該方法的宣告如下:

HRESULT SetDataFormat(
        LPCDIDATAFORMAT lpdf
)

SetDataFormat方法唯一的變數就是LPCDIDATAFORMAT型別的lpdf,DirectInput已經為我們準備好了一些備選的引數,下面是一個列表:

資料格式

精析

c_dfDIkeyboard

標準鍵盤結構,包含256個字元,每個字元對應著每個鍵

c_dfDIMouse

標準滑鼠結構,帶有3個軸和4個按鈕

c_dfDIMouse2

擴充套件滑鼠結構,帶有3個軸和8個按鈕

c_dfDIJoystick

標準遊戲杆,帶有三個定位軸,3個旋轉軸,兩個滑塊,1個POV hat和32個按鈕

c_dfDIJoystick2

擴充套件的遊戲杆


依然是一個呼叫例項,設定滑鼠的資料格式:

g_pMouseDevice->SetDataFormat(&c_dfDIMouse);


4.設定協作級別

在Windows作業系統中,系統中的每個應用程式都通常會使用多個輸入裝置,並且同一輸入裝置也可能被多個應用程式同時使用。因此,需要一種方式來共享和協調應用程式對裝置的訪問。在DirectInput中,祭出的是協作級別(Cooperative Level)這套處理方式。

協作級別定義了程序與其他應用程式和作業系統共享裝置的方式。裝置一旦建立就需要設定它的協作級別,協作級別表示了應用程式對裝置的控制權。

DirectInput的協作級別可以以兩套方案來分類:前臺、後臺模式和共享、獨佔模式。

Ⅰ.前臺模式與後臺模式

其中,前臺模式表示只有當視窗處於啟用狀態時,才能獲得裝置的控制權。而當處於非啟用狀態時,會自動失去裝置的控制權;後臺模式表示可以在任何狀態下獲取裝置,即使是在視窗處於非啟用狀態時。後天模式可以被任何應用程式在任何時候使用並獲取裝置資料。

Ⅱ.共享模式與獨佔模式

共享模式表示多個應用程式可以共同使用該裝置,而獨佔模式表示應用程式是唯一使用該裝置的應用程式。這裡需要注意一下,獨佔模式並非意味著其他應用程式不能獲取輸入裝置狀態,如果程序同時使用了後臺模式與獨佔模式的話,當其他程序申請了獨佔模式的話,這個程序就會失去裝置的控制權。

我們平常都是通過IDirectInputDevice8介面的SetCooperativeLevel方法來設定裝置的協作級別的,我們可以在MSDN中查到SetCooperativeLevel的宣告如下:

HRESULT SetCooperativeLevel(
        HWND hwnd,
        DWORD dwFlags
)

■ 第一個引數,HWND型別的hwnd,顯然就是填想要與當前裝置相關聯的視窗控制代碼了,且這個視窗需要屬於當前程序的頂級視窗。

■ 第二個引數,DWORD型別的dwFlags,描述了當前裝置的協作級別型別,也就是填我們上面講到的前臺、後臺模式和共享、獨佔模式等一些模式的識別符號,可取一個值到多個值,淺墨把取值在下表中出來了:

協作級別型別

精析

DISCL_BACKGROUND

後臺模式,一般我們讓他與DISCL_NONEXCLUSIVE(非獨佔模式)配合使用

DISCL_FOREGROUND

前臺模式,一般我們讓他與DISCL_EXCLUSIVE(獨佔模式)配合使用

DISCL_EXCLUSIVE

獨佔模式

DISCL_NONEXCLUSIVE

非獨佔(共享)模式

DISCL_NOWINKEY

讓鍵盤上煩人的Windows鍵失效

注意,後臺模式和獨佔模式不能同時選擇,用腳丫子來想都知道他們兩個組合起來不符合邏輯,既然都是在後臺了,還談什麼獨佔呢?

下面依舊是一個呼叫例項,將滑鼠裝置的協作級別設為前臺、獨佔模式:

 g_pMouseDevice->SetCooperativeLevel(hwnd,DISCL_FOREGROUND |DISCL_EXCLUSIVE);

5.設定特殊屬性

裝置的特殊屬性包含裝置的資料模式、緩衝區大小、以及裝置的最小最大範圍等等。DirectInput為我們提供了SetProperty方法來設定裝置的特殊屬性,我們可以在MSDN中查到這個方法有如下原型:

HRESULT SetProperty(

        REFGUID rguidProp,

        LPCDIPROPHEADER pdiph

)

這個方法平常用得不算多,因為篇幅原因暫且先不詳細講了,需要用的時候大家去查一下文件就可以了。

6.獲取和輪詢裝置

首先是一個常識,在訪問和使用任何輸入裝置之前,首先必須獲得該輸入裝置的控制權。權力這東西,人人都喜歡,對其趨之若鶩,在我們的計算機中也不例外。其他的程式隨時都可能勾心鬥角,爭奪並搶走對輸入裝置的控制權。所以我們在使用之前,往往都要重新獲取一下裝置的控制權,以確保權力在我們手中。

在DirectInput中,權力的敲門磚為IDirectInput8介面的Acquire方法,我們可以在MSDN中查到這個“權力權杖”有如下的原型:

HRESULT Acquire()

我們可以發現他簡簡單單清清白白,沒有引數,返回值為HRESULT。呼叫起來當然是非常簡單:

g_pMouseDevice->Acquire();

為了大家看起來簡明扼要,咱們這裡沒有用if和FAILD巨集給他括起來,進行錯誤處理。

另外需要注意的是,在獲得輸入裝置的控制權之前,必須先呼叫IDirectInputDevice8介面的SetDataFormat或者SetActionMap方法來設定一下資料格式,不然我們呼叫Acquire方法的話,將直接給我們返回DIERR_INVALIDPARAM錯誤的。

另外需要講到的是輪詢。

輪詢可以準備在合適的情況下讀取裝置資料。因為資料可能具有臨界時間的。這個輪詢的原型也是非常非常的簡單:

HRESULT Poll()

輪詢用起來當然也是非常簡單的:

g_pMouseDevice ->Poll();

7.讀取裝置資訊

在Direct3D應用程式中,拿到對輸入裝置的控制權之後,就可呼叫IDirectInputDevice8介面的GetDeviceState方法來讀取裝置的資料。而為了儲存裝置的資料資訊,在呼叫該方法時,須傳遞一個數據緩衝區給GetDeviceState方法,這個GetDeviceState方法的原型我們可以在MSDN中查到是如下:

HRESULT GetDeviceState(
        DWORD cbData,
        LPVOID lpvData
)

■ 第一個引數,DWORD型別的cbData,指定了我們緩衝區的大小(具體是哪個緩衝區在第二個引數中)。

■ 第二個引數,LPVOID型別的lpvData,表示一個獲取當前裝置狀態的結構體的地址值。

他的資料格式和我們之前呼叫的IDirectInputDevice8::SetDataFormat方法有著前後呼應的密切聯絡。下面我們通過一個表格來看看是如何聯絡的:

SetDataFormat中指定的資料格式

GetDeviceState中對應的緩衝區結構體

c_dfDIKeyboard

大小為256個位元組的陣列

c_dfDIJoystick

c_dfDIJoystick2


比如,我們先呼叫了SetDataFormat設定了裝置的資料格式為c_dfDIMouse:

g_pMouseDevice->SetDataFormat(&c_dfDIMouse);

那麼我們在讀取裝置資訊的時候呼叫GetDeviceState就需要把第二個引數填與dfDIMouse對應的DIMOUSESTATE結構體的一個例項:

DIMOUSESTATE dimouse
g_pMouseDevice-> GetDeviceState(sizeof(dimouse),(LPVOID)&dimouse);

對此,我們可以抽象出一個函式,專門對付疑難雜症,應對各種型別的裝置的資料讀取,而且還考慮到了裝置如果丟失掉了,在合適的時間自動重新獲取該裝置:

//*****************************************************************************************
// Name: Device_Read();
// Desc: 智慧讀取裝置的輸入資料
//*****************************************************************************************
BOOL Device_Read(IDirectInputDevice8*pDIDevice, void* pBuffer, longlSize)
{
         HRESULThr;
         while(true)
         {
                  pDIDevice->Poll();             // 輪詢裝置
                  pDIDevice->Acquire();          // 獲取裝置的控制權
                  if(SUCCEEDED(hr = pDIDevice->GetDeviceState(lSize, pBuffer))) break;
                   if(hr !=DIERR_INPUTLOST || hr != DIERR_NOTACQUIRED) return FALSE;
        if(FAILED(pDIDevice->Acquire())) return FALSE;
         }
         returnTRUE;
}

到這一步之後,就是呼叫一下Device_Read來讀取資料了。呼叫之後,我們的鍵位資料其實就存在了g_pKeyStateBuffer之中,我們接下來要做的就是用if語句對g_pKeyStateBuffer陣列中對應的鍵位進行試探,看看這個鍵是否被按下了。如果按下,就進行相關的處理就可以了,比如:

Device_Read(g_pKeyboardDevice,(LPVOID)g_pKeyStateBuffer,sizeof(g_pKeyStateBuffer));
 
if (g_pKeyStateBuffer[DIK_A] & 0x80)
fPosX -= 0.005f;

當然,在最後,使用完輸入裝置後,必須呼叫IDirectInputDevice8介面的Unacquire方法釋放裝置的控制權,所謂的杯酒釋兵權,且需要接著呼叫Release方法釋放掉裝置介面物件。

g_pMouseDevice->Unacquire();
g_pMouseDevice->Release();

四、精煉:DirectInput使用五步曲

上面講解了洋洋灑灑七千字,資訊量有些大,為了突出下重點,落實到一個字“用”上,讓大家有的放矢,快速掌握DirectInput的使用方法。淺墨在這裡依舊是來一個使用幾步曲的歸納,主要以程式碼為載體,把上面講得知識歸納一下。這回的DirectInput同樣是五步曲。需要說明下的是,下面的程式碼是關於處理鍵盤訊息的,而對於滑鼠裝置,需要改的地方非常少,也就是在第一步呼叫CreateDevice方法時GUID填GUID_SysKeyboard,然後在第二步SetDataFormat中填c_dfDIKeyboard就可以了(相關知識上面我們有詳細講到)。對於其他裝置。依然是改這兩個地方,其他裝置的GUID用EnumDevices列舉一下就知道了,廢話也不多說,下面就開始DirectInput使用五步曲的講解:

這五步曲分別是:

一、創鍵DirectInput介面和裝置,簡稱創裝置

二、設定資料格式和協作級別,簡稱設格式

三、獲取裝置控制權,簡稱拿權力

四、獲取按鍵情況並做響應,簡稱取按鍵

五、釋放控制權和介面物件,簡稱釋物件

DirectInput使用五步曲載體程式碼:

//首先是全域性變數的定義
LPDIRECTINPUTDEVICE8   g_pKeyboardDevice   = NULL;
char                   g_pKeyStateBuffer[256] ={0};
//--------------------------------------------------------------------------------------—----------------
//【DirectInput使用五步曲之一】,創鍵DirectInput介面和裝置,簡稱創裝置
//--------------------------------------------------------------------------------------—----------------
 
         //建立DirectInput裝置
        DirectInput8Create(hInstance,0x0800, IID_IDirectInput8,(void**)&g_pDirectInput, NULL);
        g_pDirectInput->CreateDevice(GUID_SysKeyboard,&g_pKeyboardDevice,NULL);
//--------------------------------------------------------------------------------------—----------------
//【DirectInput使用五步曲之二】,設定資料格式和協作級別,簡稱設格式
//--------------------------------------------------------------------------------------—----------------
         //設定資料格式和協作級別
         g_pKeyboardDevice->SetDataFormat(&c_dfDIKeyboard);
        g_pKeyboardDevice->SetCooperativeLevel(hwnd,DISCL_FOREGROUND |DISCL_NONEXCLUSIVE);
       
//--------------------------------------------------------------------------------------—----------------
//【DirectInput使用五步曲之三】,.獲取裝置控制權,簡稱拿權力
//--------------------------------------------------------------------------------------—----------------
g_pKeyboardDevice->Acquire();
 
//--------------------------------------------------------------------------------------—----------------
//【DirectInput使用五步曲之四】,.獲取按鍵情況並做響應,簡稱取按鍵
//--------------------------------------------------------------------------------------—----------------
       
 
// 讀取鍵盤輸入
        ::ZeroMemory(g_pKeyStateBuffer,sizeof(g_pKeyStateBuffer));
        Device_Read(g_pKeyboardDevice,(LPVOID)g_pKeyStateBuffer,sizeof(g_pKeyStateBuffer));
//定義的全域性函式
BOOL Device_Read(IDirectInputDevice8*pDIDevice, void* pBuffer, longlSize)
{
         HRESULThr;
         while(true)
         {
                   pDIDevice->Poll();              // 輪詢裝置
                  pDIDevice->Acquire();          // 獲取裝置的控制權
                  if(SUCCEEDED(hr = pDIDevice->GetDeviceState(lSize, pBuffer))) break;
                   if(hr !=DIERR_INPUTLOST || hr != DIERR_NOTACQUIRED) return FALSE;
                  if(FAILED(pDIDevice->Acquire())) return FALSE;
         }
         returnTRUE;
}
//然後就是用if判斷並做響應了,如下面一句程式碼
if (g_pKeyStateBuffer[DIK_A] & 0x80)fPosX -= 0.005f;
 
//--------------------------------------------------------------------------------------—----------------
//【DirectInput使用五步曲之五】,.釋放控制權和介面物件,簡稱釋物件
//--------------------------------------------------------------------------------------—----------------
g_pKeyboardDevice->Unacquire();
SAFE_RELEASE(g_pKeyboardDevice)

所以,上述DirectInput使用五步曲精煉總結起來就十五個字:

創裝置,設格式,拿權力,取按鍵,釋物件

五、DirectInput鍵盤按鍵鍵值總結

與一般的Windows應用程式相比,DirectInput處理鍵盤事件的方式是有很多獨特之處的。首先,在我們寫的遊戲程式中,鍵盤主要並不是用於文字輸入的,而是用於控制3D世界中人物,物件的運動,或者視角的變換等等。且在遊戲程式中我們常常只需要知道具體是哪個鍵被按下,而忽略了該鍵所對應的字元。所以我們只需讀取已按下鍵的掃描碼就可以了。

另外,為了提高程式執行的效率,DirectInput並非使用Windows中的訊息機制來讀取鍵盤的狀態,而是直接讀取硬體的狀態獲取按鍵的掃描碼的。

我們在按照流程建立好和打理好DirectInput之後,就能在程式中不斷獲取從鍵盤輸入的那些鍵盤資料。而在程式中,我們需要定義一個大小為256位元組的陣列,其中的每一個位元組都儲存一個按鍵的狀態,這樣就可以儲存256個按鍵的狀態資訊了。微軟在DirectInput中為每個鍵都設定了一個對應的巨集,這些巨集都是以DIK_為字首的。例如C鍵就定義為DIK_C,主鍵盤數字鍵8就對應DIK_8等等,下面就是淺墨對DirectInput鍵碼做的一個總結表格,查起來非常方便:

DirectInput鍵碼

說明

DIK_0~ DIK_9

主鍵盤上數字鍵0~9

DIK_A~ DIK_Z

字母鍵A~Z

DIK_F1~ DIK_F15

功能鍵F1~F15

DIK_NUMPAD0~ DIK_NUMPAD9

小鍵盤0~9

DIK_ESCAPE,DIK_TAB, DIK_BACK

Esc鍵,Tab鍵,退格鍵

DIK_RETURN,DIK_SPACE,DIK_NUMBERENTER

回車鍵、空格鍵、小鍵盤迴車鍵,

DIK_LSHIFT, DIK_RSHIFT

左右Shift鍵

DIK_LMENU, DIK_RMENU

左右選單鍵

DIK_LALT, DIK_RALT

左右Alt鍵

DIK_LCONTROL, DIK_RCONTROL

左右Ctrl鍵

DIK_UPARROW, DIK_DOWNARROW

DIK_LEFTARROW, DIK_RIGHTARROW

上下左右方向鍵

DIK_HOME, DIK_DELETE, DIK_INSERT

Home鍵,Delete鍵,Insert鍵

DIK_PRIOR, DIK_NEXT, DIK_END

PageUp鍵,PageDown鍵,End鍵

比如我們要檢測左Alt鍵是否按下,按下的話就做出響應,就可以在上表中找到左Alt鍵的鍵碼為DIK_LALT,然後就是一句if(  ){ }語句:

if (g_pKeyStateBuffer[DIK_A] & 0x80)      fPosX -= 0.005f;


六、DirectInput滑鼠按鍵鍵值總結

在通常的Windows應用程式中,系統檢測滑鼠的移動並通過訊息處理函式將滑鼠的移動作為訊息報告給使用者,然而這樣做的效率非常低下,因為傳遞給訊息處理函式的每個訊息首先都要走訊息佇列這條“官道”,需要慢悠悠地在訊息佇列中排隊,排隊完全滿足不了我們對遊戲即時處理訊息的要求。而在Direct3D中,咱們就可以屌絲逆襲走後門了,我們可以直接同滑鼠的驅動程式進行互動,而不用走訊息佇列這條慢悠悠的“官道”。

另外,我們有兩種方式來跟蹤滑鼠的移動為:絕對模式和相對模式。在絕對模式下,滑鼠是基於某個固定點的,這個點通常是螢幕左上角,而此時返回的滑鼠座標是滑鼠指標所處位置在螢幕座標系中的座標。

而另外一種模式,也就是相對模式下,滑鼠座標則是根據上一個已知位置到當前位置所發生的移動量來得到滑鼠的座標值的。在相對模式下得到的滑鼠座標是一個相對位置,而非絕對位置,大家需要注意。

好了,回到正題上來。在DirectInput中,滑鼠的移動資訊我們通常都是通過一個名叫DIMOUSESTATE結構體來記錄的,我們可以在MSDN中查到這個結構體定義如下:

typedef struct DIMOUSESTATE {
   LONG lX; 
   LONG lY;
   LONG lZ;
   BYTE rgbButtons[4];
} DIMOUSESTATE, *LPDIMOUSESTATE;

這個結構體中,lX,lY,lZ分別記錄了X軸,Y軸和Z軸(滑鼠滾輪的相對移動量,滑鼠沒移動的話,他們的值就是0.)。而結構體中的第四個引數rgbButtons[4]記錄了四個按鈕的狀態資訊,其中rgbButtons[0]代表滑鼠左鍵,rgbButtons[1]對應滑鼠右鍵。如果需要處理支援更多按鈕的滑鼠的話,就去用DIMOUSESTATE2結構體吧。

下面我們來看看例項:

DIMOUSESTATE            g_diMouseState      = {0};
::ZeroMemory(&g_diMouseState,sizeof(g_diMouseState));
Device_Read(g_pMouseDevice,(LPVOID)&g_diMouseState,sizeof(g_diMouseState));
 
         //按住滑鼠左鍵並拖動,為平移操作
         staticFLOAT fPosX =0.0f, fPosY = 30.0f, fPosZ = 0.0f;
        if(g_diMouseState.rgbButtons[0] & 0x80)
         {
                   fPosX+=g_diMouseState.lX *  0.08f;
                   fPosY+=g_diMouseState.lY * -0.08f;
         }

七、詳細註釋的原始碼欣賞

首先需要說明的是,本篇文章配套的原始碼中用到了我們目前還未講到的一點技術,就是X模型的載入。原始碼中X模型的載入相關的程式碼大家如果看不懂沒關係,請鎖定淺墨的部落格,後面一定會有相關技術精彩的講解的。 

然後這篇文章中的demo我們對細節部分做了升級,新加了三個功能,他們分別是:

1.在視窗左上角智慧讀取執行的機器使用的顯示卡名稱。

2.在視窗左下角給出了幫助資訊。

3. 在視窗左上角給出了模型當前的三維座標。

下面我們分別來對這三個新功能進行講解:

1.在視窗左上角智慧讀取執行的機器使用的顯示卡名稱。

這個其實很簡單,藉助一個GetAdapterIdentifier方法就可以了。這個方法可以獲取獲取顯示卡的廠商型別等資訊。原型如下:

HRESULT GetAdapterIdentifier(
 [in]   UINT Adapter,
 [in]   DWORD Flags,
 [out]  D3DADAPTER_IDENTIFIER9*pIdentifier
);

注意到第三個引數型別是一個D3DADAPTER_IDENTIFIER9結構體,這個結構體的第三個引數Description就儲存著顯示卡的名稱的char型別的字串。思路也就是圍繞著這個GetAdapterIdentifier方法來的,用GetAdapterIdentifier方法取得顯示卡的名稱的char型別的字串,然後轉換成wchar_t型別並在顯示卡名稱之前拼接上“當前顯示卡型號:”字樣,然後把結果存在全域性的字串陣列g_strAdapterName中,最後在Render函式中用TextOut寫出來就可以了。另外注意一點,因為IDirect3D9::GetAdapterIdentifier是IDirect3D9中的方法,而在我們的程式碼中IDirect3D9介面物件僅區域性存在於Direct3D_Init( )方法中,所以我們絕大部分實現程式碼是在這個Direct3D_Init( )方法中完成的。具體做法咱們直接看程式碼,這可是每行都詳細註釋的程式碼:

首先是一個全域性變數:

wchar_t                              g_strAdapterName[60]={0};    //包含顯示卡名稱的字元陣列

 然後就是Direct3D_Init( )方法中的功能實現程式碼:

//獲取顯示卡資訊到g_strAdapterName中,並在顯示卡名稱之前加上“當前顯示卡型號:”字串
          wchar_tTempName[60]=L"當前顯示卡型號:";  //定義一個臨時字串,且方便了把"當前顯示卡型號:"字串引入我們的目的字串中
          D3DADAPTER_IDENTIFIER9Adapter;  //定義一個D3DADAPTER_IDENTIFIER9結構體,用於儲存顯示卡資訊
          pD3D->GetAdapterIdentifier(0,0,&Adapter);//呼叫GetAdapterIdentifier,獲取顯示卡資訊
        int len =MultiByteToWideChar(CP_ACP,0,Adapter.Description, -1, NULL, 0);//顯示卡名稱現在已經在Adapter.Description中了,但是其為char型別,我們要將其轉為wchar_t型別
         MultiByteToWideChar(CP_ACP, 0,Adapter.Description, -1, g_strAdapterName,len);//這步操作完成後,g_strAdapterName中就為當前我們的顯示卡型別名的wchar_t型字串了
         wcscat_s(TempName,g_strAdapterName);//把當前我們的顯示卡名加到“當前顯示卡型號:”字串後面,結果存在TempName中
         wcscpy_s(g_strAdapterName,TempName);//把TempName中的結果拷貝到全域性變數g_strAdapterName中,大功告成~

最後就是在Direct3D_Render函式中呼叫一下DrawText顯示出來了:

//顯示顯示卡型別名
                           g_pTextAdaperName->DrawText(NULL,g_strAdapterName,-1,&formatRect,
                                    DT_TOP| DT_LEFT, D3DXCOLOR(1.0f, 0.5f, 0.0f, 1.0f));

2.在視窗左下角給出幫助資訊。

其實非常簡單,就是定義一些LPD3DXFONT介面物件,然後在Objects_Init()函式中用D3DXCreateFont建立不同的字型,最後在Direct3D_Render全DrawText出來就行了。

3. 在視窗左上角給出了模型當前的三維座標。

其實也非常簡單,就是用swprintf_s把世界矩陣g_matWorld的幾個分量格式化到一個靜態的wchar_t型別的字串中,然後DrawText出來就可以了。

實現程式碼如下:

 staticwchar_tstrInfo[256] = {0};
                           swprintf_s(strInfo,-1,L"模型座標: (%.2f,%.2f, %.2f)",g_matWorld._41, g_matWorld._42, g_matWorld._43);
                           g_pTextHelper->DrawText(NULL,strInfo, -1, &formatRect, DT_SINGLELINE| DT_NOCLIP | DT_LEFT,D3DCOLOR_RGBA(135,239,136,255));

還有一點,因為考慮到咱們的Direct3D_Render()函式中的程式碼隨著講解的不斷深入,程式碼越來越多,越來越雜,越來越亂。所以我們給他配了一個搭檔Direct3D_Update(),跟即時繪製沒有直接聯絡但是需要即時呼叫的,如按鍵後的座標的更改,按鍵後填充模式的更改等等相關的程式碼,都放在Direct3D_Update()中了,這樣就給Direct3D_Render()繪製函式減了負,看起來更加清晰。

因為也是即時呼叫,所以Direct3D_Update()在訊息迴圈中與Direct3D_Render()平起平坐了:

 //訊息迴圈過程
         MSGmsg = { 0 };  //初始化msg
         while(msg.message !=WM_QUIT )                          //使用while迴圈
         {
                  if(PeekMessage( &msg, 0, 0, 0, PM_REMOVE ) )  //檢視應用程式訊息佇列,有訊息時將佇列中的訊息派發出去。
                   {
                           TranslateMessage(&msg );            //將虛擬鍵訊息轉換為字元訊息
                           DispatchMessage(&msg );             //該函式分發一個訊息給視窗程式。
                   }
                   else