DirectInput編程基礎

導讀:
  DirectInput編程基礎 - 簡介 出 處: 中國遊戲開發者
  [ 2001-09-09] 做 者
  目 錄
  1.1 DirectInput概念
  1.2 設置DirectInput
  1.3 列舉設備
  1.4 設置設備
  1.5 取得輸入數據
  緒言
  輸入相對於圖形和聲音而言從未成爲遊戲開發中的很是重要的論題。讀取鍵盤按鍵、鼠標移動和遊戲杆位置彷佛並無什麼困難,但隨着新輸入設備對市場的強烈衝擊以及DirectX的發行,這一問題變得日益重要了。
  若是用戶是DirectInput編程的新手,那麼應先排除掉一些舊觀念。應記住的重要一點是DirectInput的得名是由於它直接與設備驅動器通信,對鼠標和鍵盤也就意味着當即響應硬件中斷,而不是等待Windows發送消息證實已發生輸入事件。固然,DirectInput放棄了Windows的消息隊列,在虛構WM_KEYDOWN或WM_MOVSEMOVE等消息的同時也就再也不享受Windows提供的一些服務。這種服務大都不是對遊戲輸入特別有用,但它有助於理解缺乏這些服務時所產生輸入信息的含義,後面有關鼠標和鍵盤輸入的章節中,將深刻地討論這一問題。
  固然,DirectInput放棄了Windows的消息隊列,在虛構WM_KEYDOWN及WM_MOVSEMOVE等消息的同時也就再也不享受Windows提供的一些服務。這種服務大都不是對遊戲輸入特別有用,但它有助於理解缺乏這些服務時所產生輸入信息的含義,後面有關鼠標和鍵盤輸入的章節中,將深刻地討論這一問題。
  1.一、DirectInput概念
  下面看一下DirectInput所關心的內容:輸入設備及其部件。
  1.1.1 設備
  DirectInput可識別三種基本設備類型:
  鍵盤,標準系統鍵盤。
  鼠標,這一類中包括全部相似於鼠標的設備,如觸摸板、跟蹤球以及相應的按鍵。
  遊戲杆,相對於其它類型的控制器和力反饋設備而言,這是易於產生誤解的名稱,其範圍從簡單的遊戲桿直至虛擬現實的複雜設備。本書中,咱們使用這一名稱是爲了與DirectInput術語保持一致,但應記住,當談到「遊戲杆」時,這一說法通常是「特殊遊戲控制器」的簡稱。  只要講到DirectInput,那麼設備就是指由設備驅動器表明的螺絲、螺母集合, 帶有內置跟蹤球或觸摸板的膝上鍵盤算做兩個設備,但帶有力反饋激勵器的遊戲杆倒是一個設備。DirectInput API和文檔中把返回數據或產生力反饋效果的設備的不一樣部分都稱爲設備對象實例或簡稱對象,這兩種叫法都不是最佳的。由於「對象」一詞(在其它狀況下)指代碼對象,「實例」批COM接口的例示。在本文中,咱們儘可能避免使用這種叫法,而用更準確的「設備物」取代。咱們認爲,設備物能夠是任何的鍵、按鈕、視點帽或軸等。
  另外一個更易混淆的問題是「設備」能夠指物理對象(如其驅動器)也能夠指DirectInput建立的DirectInputDevice對象。當講到建立一個設備時,咱們指得到一個指向IDirectInputDevice接口的指針,以即可以用相應方法與設備驅動器通信。
  1.1.2 按鈕和軸
  按鈕是任何具備開和關狀態的設備。DirectInput在概念上不對按鈕加以區分,它們能夠位於鼠標上、遊戲杆上或遊戲板上,還能夠是鍵盤上的鍵。
  軸是設備中的一種控制而不是按鈕 — 有位置而不是開、關狀態的事物。嚴格地說,軸不是物理對象,而是由物理對象向某一方向的移動產生的一個值。遊戲杆、跟蹤球、鼠標球和觸摸板等一個物理對象控制兩個軸的值,分別稱爲X軸和Y軸,由於它們通常控制的是二維計算機屏幕上的指針或遊戲元素。遊戲杆上的減速把手或滑塊只控制一個軸,它與MicrosoftIntelliMouse鼠標中的轉輪是同樣的。遊戲杆上也可能有其它的軸,如用於控制舵方向的扭轉動做等,用於駕駛、飛行摸擬等的特殊控制器依靠操縱輪、軛、踏板等操縱多個軸。
  視點帽是例外的一種狀況,它不是有任何按鈕或軸,後面有關遊戲杆的章節中將詳細討論這一設備。應記住的重要一點是,軸與物理空間或其它事件之間不存在固定關係。
  程序上下文中設備的Z軸(儘管與三維座標系統中一個軸的名稱相同)可能與空間的第三維毫無關係,而其做用卻多是控制翻頁、音量或其它非空間屬性。雖然已有一些成形的約定,如把X 軸與控制器的左右運動和屏幕上的水平方向相關聯,但映射設備中的物理量與DirectInput中邏輯軸的關係是由設備驅動器實現的,而把這些軸與它們發揮做用的遊戲概念關聯起來是由程序設計者完成的 — 或給使用者以選擇的機會並由他們完成。
  軸能夠是絕對的或相對的。遊戲杆中的X軸和Y軸天生是絕對的,由於在任何方向它們都只能移動有限的距離,他們的位置也一般是以與某一固定中心點的距離而測量的。相反,鼠標天生是一種相對的設備,由於它能夠在任何方向上無限地移動,並且也設有「原點」,對它所關心的只是在上一次檢測以後它沿每一個軸運行的距離。
  DirectInput能夠違反天性而把任何軸看成絕對的或相對的來處理。但多數狀況下把鼠標(包括轉輪)處理爲絕對的軸,把其它設備 — 杆、滑塊、撥號盤看成相對軸處理,也就是說,對鼠標要考慮的是其運動,而其它軸考慮的則是位置。
  1.二、設置DirectInput
  簡單地調用DirectInputCreate函數(見表1-1)就能夠初始化DirectInput系統,此函數將返回一個指向IDirectInput接口的指針,而後能夠用此接口的方法列舉設備並建立設備對象,這些設備對象是DirectInput的工做部分。
  表1-1 DirectInputCreate函數
  HRESULT WINAPI DirectInputCreate(
  HINSTANCE hinst,
  DWORD dwVersion,
  LPDIRECTINPUT * lplpDirectInput,
  LPUNKNOWN punkOuter
  );
  參數 說明
  HINSTANCE hinst 建立DirectInput對象的程序或DLL的實例句柄
  DWORD dwVersin 設計程序所用的DirectInput版本號,此值一般爲DIRECTINPUT_VERSION,但看到的是文本
  LPDIRECTINPUT *lplpDirectInput 新DirectInput對象的指針地址
  LPUNKNOWN pUnkOuter 必須爲NULL
  注意:DirectX很是靈活地容許用戶用之前的版本(實際即爲版本3)進行設計,只要給DirectInputCreate函數傳遞一個非DIRECTINPUT_VERSION值就好了。若是傳遞的是OXO300,則程序在裝有DirectX 3運行文件的系統中會很好地運行。但必須確保在所用結構與DirectX與定義的不一樣時,使用與DirectX3兼容的結構,如DIDEVCAPS_DX3。
  簡單有效的方法是在包含DINPUT.H文件前給出本身對DIRECTINPUT_VERSION的定義,如:如今不用擔憂使用舊結構了,由於DINPUT.H被解釋過之後,所存的新結構都奇蹟般地轉換成對應的早期結構,其中DIDEVCAPS與DIDEVCAPS_DX3一致,如此類推。
  提示:即便是用新版DirectInput進行設計,也可使用舊版本中的結構,用途之一是IDirectInputDevice:: GetCapabilities方法,經過傳遞傳來DIDEVCAPS_DX3結構將省去DirectInput查詢力反饋驅動器的一步,可以使速度略有提升。固然只有在對力反饋不感興趣時纔可這樣作,同時不要忘記初始化dwSize成員爲sizeof(DIDEVCAPS_DX3)。
  1.三、列舉設備
  你通常不須要列舉鼠標和鍵盤。即便與用戶系統鏈接的這兩種設備都不止一個(固然這種可能很小),咱們也能夠經過在IDirectInput::CreateDevice中使用預約義的變量GUID_SYSMOUSE 和GUID_SYSKEYBOARD來解決。
  遊戲杆則屬於另外一種狀況,由於不存在所謂的「系統遊戲杆」。一些用戶會安裝多種遊戲控制器的驅動程序,甚至把多個這種設備鏈接到系統。至少,爲了得到了一個合適的設備或多個設備的GUID,要對其進行列舉,或許爲了使用戶能進行選擇還須要列舉全部可用的設備。後面有關遊戲杆的章節中將詳細討論這一問題。
  要列舉設備須要先創建一個回調函數,而後調用EnumDevices方法,見表1-2。
  表1-2 EnumDevices方法
  HRESULT IDirectInput::EnumDevices(
  DWORD dwDevType,
  LPDIENUMCALLBACK lpCallback,
  LPVOID pvRef,
  DWORD dwFlags
  );
  參數 說明
  DWORD dwDevType DIDEVTYPE_* 值,指定要查找的設備類型,如爲0則查找全部類型
  LPDIENUMCALLBACK lpCallback 回調函數地址
  LPVOID pvRef 可爲任何值,典型值爲一指向數據的指針,可在回調函數中使用或修改。這是傳給回調函數的第二個參數
  DWORD dwFlags 對列舉的更嚴格的限制標誌。能夠爲(a)DIEDFL_ALLDEVICES(=0)或(b)DIEDFL_ATTACHEDONLY和DIEDFL_FORCEFEEDBACK之一或二者皆有
  EnumDevices用到的回調函數能夠有許多用途。被列舉的特定設備的許多信息都在DIDVICEINSTANCE結構中,此結構由DirectInput自動創建並傳給回調函數,下面是回調函數如何使用這些信息的幾個例子:
  1)建立此設備。此設備的實例GUID已位於DIDEVICEINSTANCE結構中,所以回調函數能夠很方便地獲取IDirectInputDevice接口並進行其它初始化工做。
  2)檢查設備的子類型,例如,能夠檢查鍵盤子類型並對遊戲功能對應的鍵作必要的修改(可是,若是列舉鍵盤沒有別的重要目的,則能夠經過調用IDirectInputDevice::GetDeviceInfo簡單地實現)。下面的例子說明了如何在回調函數中更改對鍵功能的分配。這裏假定調用EnumDevices時已限定了列舉的是鍵盤。
  BOOL CALLBACK DIEnumKbdProc( LPCDIDEVICEINSTANCE lpddi,
  LPVOID pvRef )
  {
  switch ( GET_DIDEVICE_SUBTYPE( GET_DIDEVICE_SUBTYPE( lpddi->dwDevType ))
  {
  case DIDEVTYPEKEYBOARD_PCXT;
  case DIDEVTYPEKEYBOARD_PCAT;         // No F11 and F12 keys. so reassign here.
  ...         break;
  // And so on. ALL the subtypes are listed in the
  // reference for the DIDEVICEINSTANCE structure.
  ...    }
  return DIENUM_CONTINUE;
  }  3)檢測設備性能。假設如今正在查找有至少8個按鈕的遊戲杆,對列舉的每種遊戲杆均可以建立一個臨時的IDirectInputDevice接口並調用其GetCapabilities方法,所需的信息將由此方法填入DIDEVCAPS結構的dwButtons成員中。
  4)產生一個列表框,用戶可從中選擇一種設備。
  1.四、設置設備
  準備用DirectInput使用設備有時有點象飛行前的檢查清單,但不要把這當成雜事 — 應把它當作一種機會,一種避免錯誤發生,使DirectInput正常工做的機會。
  1.4.1 建立設備
  選定一種輸入設備或由用戶選出一種後,接下來能夠爲其獲取一個接口。繼續工做以前所需的一條基本信息是設備的實例GUID,若是沒有在列舉時從DirectInput傳給回調函數的DIDEVICEINSTANCE結構的guidInstance成員中獲取這一GUID,則須要使用預約義變量GUID_SysKeyboard或GUID_SysMouse。表1-3爲 CreateDevice的方法。
  表1-3 CreateDevice方法
  HRESULT CreateDevice(
  REFGUID rguid,
  LPDIRECTINPUTDEVICE *lplpDirectInputDevice,
  LPUNKNOWN pUnkOuter
  );
  參數 說明
  REFGUID rguid 設備的實例GUID
  LPDIRECTINPUTDEVICE *lplpDirectInputDevice 指向新IDirectInputDevice接口的指針地址
  LPUNKNOWN pUnkOuter 必須爲NULL
  注意:GUID是按引用傳遞的,這也許是由於GUID_SysKeyboard和GUID_SysMouse是全局變量而不是宏。在C中,不容許按引用傳遞,所以只能傳遞GUID的指針。
  得到IDirectInput Device接口後,一般應當即查詢IDirectInputDevice2接口並代替IDirectInput接口。這一步對於力反饋編程很是重要,但即便不須要力反饋,也還可能想用IDirectInputDevice2::Poll方法,其緣由稍後再解釋。下面的程序先建立設備,而後爲其獲取IDirectInputDevice2接口。讀者能夠在其中加入本身的錯誤處理語句。
  LPDIRECTINPUTDEVICE2 CreateDevice2( LPDIRECTINPUT lpdi, GUID* pguid )
  {
  HRESULT hr, hr2;
  LPDIRECTINPUTDEVICE lpdid1; // Temporay.
  LPDIRECTINPUTDEVICE2 lpdid2; // The keeper.
  hr = lpdi->CreateDevice( *pguid, &lpdid1, NULL );
  if ( SUCCEEDED( hr ) )
  {
  hr2 = lpdid1->QueryInterface( IID_IDirectInputDevice2,
  ( void ** )&lpdid2 );
  lpdid1->Release();
  }
  else
  {
  OutputDebugString(
  "Could not create IDirectInputDevice device" );
  return NULL;
  }
  if ( FAILED( hr2 ) )
  {
  OutputDebugString(
  "Could not create IDirectInputDevice2 device" );
  return NULL;
  }
  return lpdid2;
  }// CreateDevice2.
  1.4.2 設置數據格式
  從設備獲取數據以前,必須先設置其數據格式。這只是一個在設備的狀態改變後獲取數據包並與已知結構進行匹配的問題。例如,從遊戲杆獲得的數據包顯然和從鼠標那裏得來的不同:格式的大小不一樣,而且按照按鈕和軸組合的不一樣有不一樣格式大小的值。表1-4中的SetDataFormat方法很是簡單。
  表1-4 SetDataFormat方法
  HRESULT IDirectInputDevice::SetDataFormat(
  LPCDIDATAFORMAT lpdf
  );
  參數 說明
  LPCDIDATAFORMAT lpdf DIDATAFORMAT結構的地址
  若是讀者看一下DIDATAFORMAT的文檔,會看到錯綜複雜的說明:其中給出了有關DIOBJECTDATAFORMAT結構的一個數組的信息,說明了設備上每一按鈕的數據是如何安排的。
  爲何要如此複雜呢?由於DirectInput爲目前還未設計出的設備留有餘地,這種設備返回的數據可能與慣用結構不符。設置自定義的數據格式是至關複雜的。但幸運的是, DirectInput開發者提供了四種預約義數據格式,可用於任何標準輸入設備。它們都是全局變量,都爲設備描述了一種數據結構。用戶所要記住的是,當設置一個預約義的數據格式時,當即會有數據返回到表1-5中所示的標準結構中。
  表1-5 預約義的全局數據格式變量
  全局DIDAFORMAT 數據結構
  c_dfDIMouse DIMOUSESTATE
  c_dfDIKeyboard char[256]
  c_dfDIJoystick(適用於多數遊戲控制器) DIJOYSTATE
  c_dfDIJoystick2(適用於標準遊戲控制器) DIJOYSTATE2
  設置遊戲杆的數據很簡單,見下面的例子:
  // lpdid is the IDirectInputDevice interface.
  lpdid->SetDataFormat( &c_dfDIJoystick );
  注意:即便不打算調用IDirectInputDevice::GetDeviceState也要爲設備設置數據格式,DirectInput對這些數據格式有其它用途。
  1.4.3 獲取設備信息
  如今有必要對從不一樣設備和「設備對象實例」即設備物中提取信息的各類方法以及設備支持的力反饋效果作一律述,見表1-6。
  表1-6 用於獲取各類信息的DirectInput方法
  方法· 父接口 目的
  GetDeviceStatus IDirectInput 用戶傳來設備的實例GUID,若是該設備鏈接到系統上,則此方法返回DI_OK(注意該設備不必定非得建立爲DirectInput設備)
  EnumObjects IdirectInputDevice 列舉設備中的按鈕,軸和視點帽
  GetCapabilities IdirectInputDevice 結構中,包括設備是否已鏈接,是否支持某種力反饋參數,以及按鈕、軸、視點帽的數量
  GetDeviceData IdirectInputDevice 取得源於設備的緩衝區數據
  GetDeviceInfo IdirectInputDevice 把多種信息與寫DIDEVICEINSTANCE結構中,包括設備的產品和實例GUID類型、暱稱及標題等
  GetDeviceState IdirectInputDevice 取得直接從設備而來的輸入數據——即各按鈕、軸、視點帽的當前狀態
  GetObjectInfo IdirectInputDevice 把特定按鈕或軸的信息寫入DIDEVICEOBJECTINFO結構中,包括類型、在數據格式中的位置、暱稱、對輸入數據某些更祕密條目的支持等
  GetProperty IdirectInputDevice 取得設備屬性 — 便可用SetProperty改變的行爲,如怎樣說明軸的數據等
  EnumCreatedEffectObjects IdirectInputDevice2 列舉已建立的力反饋效果
  EnumEffects IdirectInputDevice2 列舉設備支持的
  GetEffectInfo IdirectInputDevice2 將所支持的一種力反饋效果的各方面信息寫入DIEFFECTINFO結構中
  GetforceFeedbackState IdirectInputDevice2 取得力反饋設備的信息,包括是否已加電,是否處於暫停狀態等
  
  (·)全部IDirectInputDevice方法對IDiretInputDevice2和接口一樣有效。
  列舉設備物。建立設備後的第一步通常應該是找出有哪些可用的按鈕,軸或視點帽。在示例程序DInput中,每當用戶選定新設備後即進行這一操做,以使用戶能選擇發射鍵。表面上看EnumObjects方法是用於取得可用設備物清單的方法,但到底是不是這樣呢?先看一看錶1-7中此方法的語法。
  表1-7 EnumObjects方法
  HRESULT IDirectInputDevice::EnumObjects(
  LPDIENUMDEVICEOBJECTSCALLBACK lpCallback,
  LPVOID pvRef,
  DWORD dwFlags
  );
  參數 說明
  LPDIENUMDEVICEOBJECTSCALLBACK lpCallback 回調函數地址
  LPVOID pvRef 可爲任何值,典型值爲一指向數據的指針,可在回調函數中使用或修改。此值是傳給回調函數的第二個參數
  DWORD dwFlags DIDFT_* 標誌的組合,以使列舉侷限於某種類型的設備,如可產生力反饋效果的按鈕
  下例的調用將列舉全部按鈕,包括鍵盤上的鍵:
  g_1pdid2->EnumObjects(EnumDeviceObjectsProc,NULL,DIDFT_BUTTON)  通常列舉方法中的第一個參數是指向回調函數的指針,每次DirectInput發現匹配設備時都將調回此函數。中間的參數是可任意使用的pvRef,通常用做指向數據的指針,且此數據須要在回調函數中使用或修改。
  回調函數照例必須接收一種標準序列的參數並返回一個BOOL值(DIENUM_CONITINUE或DIENUM_STOP),但在其它方面則徹底由開發者決定其功能,下面是一個回調函數的例子:
  BOOL CALLBACK EnumDeviceObjectsProc( LPCDIDEVICEOBJECTINSTANCE lpddoi, LPVOID pvRef )
  {
  int i;
  i = SendDlgItemMessage( g_hOptionsWnd,
  IDC_COMBO_BUTTONS,
  CB_ADDSTRING,
  0,
  ( LPARAM )( lpddoi->tszName ) );
  SendDlgItemMessage( g_hOptionsWnd,
  IDC_COMBO_BUTTONS,
  CB_SETITEMDATA,
  i,
  ( DWORD )lpddoi->dwType )
  return DIENUM_CONTINUE; // = TRUE.
  }
  IDirectInput::EnumDevices調用回調函數後,DirectInput將其列舉的特定設備物的信息寫入到一個結構中。在上述狀況下,此結構是DIDEVICEOBJECTINSTANCE,見表1-8。
  表1-8 DIDEVICEOBJECTINSTANCE 結構
  typedef struct DIDEVICEOBJECTINSTANCE {
  DWORD dwSize;
  GUID guidType;
  DWORD dwOfs;
  DWORD dwType;
  DWORD dwFlags;
  TCHAR tszName[MAX_PATH];
  DWORD dwFFMaxForce;
  DWORD dwFFForceResolution;
  WORD wCollectionNumber;
  WORD wDesignatorIndex;
  WORD wUsagePage;
  WORD wUsage;
  DWORD dwDimension;
  WORD wExponent;
  WORD wReportId;
  } DIDEVICEOBJECTINSTANCE, *LPDIDEVICEOBJECTINSTANCE;
  typedef const DIDEVICEOBJECTINSTANCE *LPCDIDEVICEOBJECTINSTANCE;
  成員 說明
  DWORD dwSize 若是結構傳給IDirectInputDevice::GetObjectInfo,則此值必須初始化爲sizeof(DIDEVICEOBJECTINSTANCE)
  GUID guidType 區分對象是鍵、按鈕仍是軸等
  DWORD dwOfs 數據格式中的首選(沒必要是實際的)偏移量,對象數據可在此處取得。只有使用自定義數據格式的程序才需考慮這一點
  DWORD dwType 更多有關對象類型和對象實例標識的信息。(參見本章後部的「標識設備物」)
  DWORD dwFlags 對象的其它信息,主要用於處理對力反饋的支持
  TCHAR tszName[MAX_PATH] 對象的呢稱 — 如「Dial」
  DWORD dwFFMaxForce 對象可輸出的實際做用力,以牛頓爲單位
  DWORD dwFFForceResolution 輸出做用力的分級間隔,以萬分之幾表示
  WORD wCollectionNumber 保留以支持HID
  WORD wDsignatorIndex 保留以支持HID
  WORD wUsagePage 保留以支持HID
  WORD wUsage 保留以支持HID
  DWORD dwDimension 對像值所採用的尺寸單位
  WORD wExponent 與尺寸相關的指數(若是已知的話)
  WORD wExponent 保留
  稍後將用到此結構,由於它頗有用 — 但不是由於EnumObjets,爲何呢?回憶一下如何給設備設置數據格式。設置完成後DirectInput對這種設備的印象就被鎖定住了。例如,若是把數據格式關聯到DIJOYSTATE結構,則DirectInput將準備好從多達32個按鈕,6條軸,2個滑塊,4個視點帽獲取數據。通常說來設備上擁有的對象遠少於此,但它表示的是設備能夠多擁有一些。
  必需要理解實際設備與DirectInput對它的印象之間的差異。EnumObjects列舉設備上的全部東西而無論DirectInput是否準備從其中獲取數據。實際上,DirectInput甚至可能連此設備的數據格式都沒有;即便有,也沒法從數據結構中的某個特定區域識別出所列舉的對象。DIDEVICEOBJECTINSTANCE中的dwOfs成員指明瞭DirectInput會首選地把數據放在什麼地方(以免直接對驅動器提供的原始數據包進行拼揍和排序),對標準數據格式而言就是其結束的位置,但並不能確保是這樣的。
  只有那些須要不停地建立自定義的數據格式的程序纔會正規地調用EnumObjects。好比如今想列舉真正在於數據結構中的遊戲杆按鈕,那麼應該象下面這樣進行:
  void CountJoyButtons( HWND hwnd )
  {
  DIDEVICEOBJECTINSTANCE
  DWORD i, X;
  DWORD MaxButtons = 32; // if DIJOYSTATE is our format.
  DWORD dwOfs;
  didoi.dwSize = sizeof( didoi );
  for( x = 0; x
  {
  dwOfs = DIJOFS_BUTTON( x );
  if ( SUCCEEDED( g_lpdid2->GetObjectInfo( &didoi,
  dwOfs,
  DIPH_BYOFFSET ) ) )
  {        
  i = SendDlgItemMessage ( hwnd, IDC_COMBO_BUTTONS,
  CB_ADDSTRING, 0,
  ( LPARAM ) didoi.tszName );
  SendDlgItemMessage ( hwnd, IDC_COMBO_BUTTONS,
  CB_SETITEMDATA, i, dwOfs );
  }
  }
  }// CountJoyButtons().
  此函數使用DINPUT.H中的一個宏(根據合適的數據格式)循環檢測可能存在的按鈕並肯定其偏移值(下一節將講到偏移量標識問題)。而後調用IDirectInputDevice::GetObjectInfo,若是按鈕存在將返回DI_OK,不然返回DIERR_OBJECTNOTFOUND。
  若是按鈕存在,DirectInput將返回相關信息並存入DIDEVICEOBJECTINSTANCE結構中 — 與EnumObjects填充的結構同樣。接下來CountJoyButtons獲取按鈕的暱稱並將其填入對話框的下拉列表框中,此對話框所屬的窗口的句柄就是傳給上面函數那個句柄。同時它還將偏移值做爲列表項的相關數據而存儲,用索引使得在列表中的列表項增長或存儲後能方便地取得這一偏移值。此偏移值用於標識用戶選定的按鈕。
  1.4.4 標識設備物
  上一節講過按鈕或其它對象能夠在調用GetObjectInfo時用設備數據結構中的偏移量來標識,這是標識設備物的兩種方法之一,另外一種方法是由實例號即ID標識。
  由偏移量標識。給設備設定數據格式後,每一對象都關聯到此設備數據格式的特定位置處,此位置的字節偏移量是一種方便地標識對應設備物的方法。
  如今來考慮一種最簡單的狀況:鍵盤。每次讀鍵盤狀態時都會返回一個256字節的數組(稱之爲KeyArray)。每一鍵都在數組中有一個索引值,對應於從數組起點開始的偏移量,這些索引值在DINPUT.H中以DIK_* 的宏定義形式給出。如ESC鍵對應的字節數據在KeyArray[DIK_ESCAPE]中(第4章鍵盤輸入中有DIK_*值的完整列表)。但DIK_ESCAPE也能夠在另外的狀況下標識ESC鍵,如如今要從源於鍵盤的緩衝區數據中獲取一些事件的信息,這些事件各自與一單獨的鍵有關。若是ESC鍵被按下,則數據包中的標識爲DIK_ESCAPE(稍後再討論有關獲取數據的內容,上面只是爲了說明如何用數據偏移量標識鍵)。
  鼠標和遊戲杆數據也相似,只不過數據格式更加複雜。對鼠標而言,數據格式是DIMOUSESTATE結構,包括三個LONG(對應於每一可能的軸)和四個字節(對應於每一可能的按鈕)。對應於主按鈕的數據偏移量是從DIMOUSESTATE結構起點開始的rgbButtons[0]的偏移量。要得到幫助能夠查DINPUT.H中的宏。
  鼠標偏移量。對遊戲杆而言,根據設定的數據格式,偏移量既能夠是相對於DIJOYSTATE結構起點也能夠相對於DIJOYSTATE2結構起點,DINPUT.H中提供了從任一遊戲杆數據結構中獲取設備物偏移量的宏定義。
  DIMOFS_BUTTON0
  DIMOFS_BUTTON1
  DIMOFS_BUTTON2
  DIMOFS_BUTTON3
  DIMOFS_X
  DIMOFS_Y
  DIMOFS_Z   遊戲杆偏移量
  DIJOFS_BUTTON0到DIJOFS_BUTTON31
  DIJOFS_BUTTON(n)
  DIJOFS_POV(n)
  DIJOFS_RX
  DIJOFS_RY
  DIFOFS_RZ
  DIJOFS_X
  DIJOFS_Y
  DIJOFS_Z
  DIJOFS_SLIDER(n)  注意:遊戲杆按鈕既能夠由常量標識也能夠由索引標識。如按鈕0能夠對應DIJOFS_BUTTON0或DIJOFS_BUTTON(0)。與鍵盤的狀況相似,鼠標或遊戲杆上按鈕或軸的數據偏移量用於把存於緩衝區的輸入項關聯到產生這些輸入的設備物上,在獲取或設置屬性時也能夠用這些偏移量把其中的一條軸單列出來。
  由ID標識標識。設備物的另外一種方法是經過實例號(或ID)進行,它只是分配給各設備物一個序列,但不是GUID。回憶一下前面講過的應分清DirectInput對設備的印象與由驅動器表示的設備實際組成之間的差異。設備上的每一對象都有實例號,即便數據結構中設有此對象的位置也是這樣,甚至在根本未給此設備設置數據結構時也是這樣。當從DIDEVICEOBJECTINSTANCE結構中獲取設備物的信息時,不管是經過列舉仍是經過調用IDirectInputDevice::GetObjectInfo方法進行,標識信息將返到dwType成員中。此DWORD實際包含有兩條信息,低字節存儲的是對象的類型(如軸或按鈕),中間兩個字節爲實例號,能夠用DIDFT_GETTYPE和DIDFT_GETINSTANCE來提取所需的信息。
  偏移量與ID:哪一個更好。多數狀況下,選擇偏移量仍是實例號來識別設備物是可有可無的。用諸如GetObjectInfo及SetProperty等方法時可使用其中任一種,但必需要在適當位置設置DIPH_BYOFFSET或DIPH_BYID標誌以使DirectInput知道選用的是哪種數值。但要獲取輸入數據,則應用偏移量來標識設備物。在程序中使用偏移量系統是最簡單的,只有在訪問特定做用返饋設備上的只輸出對象的屬性時才絕對要使用ID號,由於只輸出設備不能提供輸入數據,所以也就沒有數據偏移量。
  1.4.5 設置和獲取設備屬性
  設備屬性可包括如下內容:
  軸如何提供數據(相對或絕對)。
  存入緩衝區的輸入數據對應的緩衝區大小。
  軸的物理範圍和它提供的數據之間的關係(範圍、飽和度及盲區)。
  力反饋杆是否模擬自居中彈性。
  力反饋設備的增益(相似於音量控制)。  另外,製造商可爲特定設備定義其它屬性。表1-9介紹了SetProperty及GetProperty屬性。
  表1-9 SetProperty和GepProperty屬性
  HRESULT GetProperty(
  REFGUID rguidProp,
  LPDIPROPHEADER pdiph
  );
  及
  HRESULT GetProperty(
  REFGUID rguidProp,
  LPDIPROPHEADER pdiph
  );
  參數 說明
  REFGUID rguidProp 要設置或獲取屬性的標識,能夠爲DINPUT.H中的DIPROP_* 預約義值,也能夠是製造者提供的GUID
  LPDIPROPHEADER pdiph 主數據結構中DIPROPHEADER結構的地址,一般是一個DIPROPDWORD或DIPROPRANGE。主結構中的其它成員含有要被設置的數據
  和別處同樣,DirecX靈活的有遠見的天性使得它看起來比想象得要複雜得多。因爲SetProperty可用於人們目前根本沒有想到的設備,因此不能讓它接受標準數據結構。實際上,它能夠接受任何針對要訪問的屬性而預約義的結構。對標準屬性,能夠是DIPROPDWORD(任何須要一個DWORD的屬性),或DIPROPRANGE(任何須要兩個LONG的屬性)。但製造商能夠聲明任何其它類型的結構,僅需以一個DIPROPHEADER結構(見表1-10)做爲其數據的第一部分,DirectInput能知道主結構的大小並標識出要設置或獲取其屬性的對象。
  表1-10 DIPROPHEADER結構
  typedef struct DIPROPHEADER {
  DWORD dwSize;
  DWORD dwHeaderSize;
  DWORD dwObj;
  DWORD dwHow;
  } DIPROPHEADER, *LPDIPROPHEADER;
  typedef const DIPROPHEADER *LPCDIPROPHEADER;
  成員 說明
  DWORD dwSize 必須初始化爲外圍結構的大小,如sizeof(DIPROPDWORD)
  DWORD dwHeaderSize 必須初始化爲sizeof(DIPROPHEADER)
  DWORD dwObj 屬性被訪問的設備物的標識,若是包含整個設備則爲O(對設定緩衝區大小而言)
  DWORD dwHow 若是包含整個設備則爲DIPH_DEVIC,不然爲DIPH_BYOFFSET或DIPH_BYID,說明採用哪一種系統標識設備物
  表1-11與1-12爲常常與SetProperty和GetProperty一同使用的兩個標準外圍結構。DIPROPDWORD用於設置或獲取任何須要一個DWORD數據的屬性;DIPROPRANGE用於設置或獲取絕對軸的範圍,也能夠用於訪問任何使用兩個LONG數據的屬性。關於軸的範圍將在第4章遊戲杆輸入中詳細討論。
  表1-11 DIPROPDWORD結構
  typedef struct DIPROPDWORD {
  DIPROPHEADER diph;
  DWORD dwData;
  } DIPROPDWORD, *LPDIPROPDWORD;
  typedef const DIPROPDWORD *LPCDIPROPDWORD;
  成員 說明
  DIPROPHEADER diph 前面進過的結構頭
  DWORD dwData 數據 — 由人設置並經過SetProperty傳給設備,或由Getproperty填充。典型的數據項爲緩衝區大小和軸模式等,數據的意義由方法的rguidProp參數決定
  表1-12 DIPROPRANGE結構
  typedef struct DIPROPRANGE {
  DIPROPHEADER diph;
  LONG lMin; LONG lMax;
  } DIPROPRANGE, *LPDIPROPRANGE;
  typedef const DIPROPRANGE *LPCDIPROPRANGE;
  成員 說明
  DIPROPHEADER diph 結構頭
  LONG lMin 最小範圍值
  LONG lMax 最大範圍值
  下面是用SetProperty設置X軸範圍的一個例子:
  #define JOYMIN -1000
  #define JOYMAX 1000
  DIPROPRANGE diprg;
  diprg.diph.dwSize     = sizeof( diprg );
  diprg.diph.dwHeaderSize  = sizeof( diprg.diph )
  diprg.diph.dwObj     = DIJOFS_X
  diprg.diph.dwHow     = DIPH_BYOFFSET;
  diprg.lMin        = JOYMIN;
  diprg.lMax        = JOYMAX;
  if ( FAILED( g_lpdid2->SetProperty( DIPROP_RANG, &diprg.diph ) ) )
  retrurn FALSE;
  接下來要獲取設備的緩衝區大小,這裏DIPROPDWORD結構在聲明中初始化,但其思想與上一例基本同樣。
  DIPROPDWORD dipdw =
  {    // The header, which we initialize.
  {
  sizeof( DIPROPDWORD ),  // diph.dwSize      sizeof( DIPROPHEADER ),  // diph.dwHeaderSize      0,            // diph.dwObj - no particular object      DIPH_DEVICE,       // diph.dwHow - entire device    };
  // The data will go here.
  0               // dwData
  };
  HRESULT hr = g_lpdid2->GetProperty( DIPROP_BUFFERSIZE, &dipdw.diph );
  1.5.6 協做級別
  象每次使用結構時都要填充dwSize成員同樣,設置協做級別也是一種使人費解的工做 — 爲何非得由用戶作這項工做?難道DirectInput不能作得更好嗎?事實上,DirectInput很是但願把事情作得更好,但有時也須要一些幫助。設置設備的協做級別旨在讓DirectInput知道用戶所但願的應用程序與系統以及其它程序之間相互聯繫時有什麼樣的表現。在深刻講解這一問題以前,先介紹一下程序調用IDirectInputDevice::SetCooperativeLevel時能使用的有效標誌組合,見表1-13。
  表1-13 IDirectInputDevice::SetCooperativeLevel使用的標誌組合
  標誌 含義 適用於
  DISCL_NONEXCLUSIVE|DISCL_ABCKGROUND 其餘人能夠以獨佔或非獨佔模式得到設備;當前程序能夠在任什麼時候刻訪問數據 除力反饋外的全部設備
  DISCL_NONEXCLUSIVE|DISCL_FOREGROUND 其餘人能夠以獨佔或非獨佔模式得到設備;當前程序只有在位於前臺時才能訪問數據 除力反饋外的全部設備
  DISCL_EXCLUSIVE|DISCL_BACKGROUND 其餘人能夠以非獨佔模式得到設備;當前程序能夠在任什麼時候刻訪問數據遊戲杆和力反饋設備
  DISCL_EXCLUSIVE|DISC_FOREGROUND 其餘人能夠以非獨佔模式得到設備;當前程序只有在位於前臺時才能訪問數據除鍵盤外的全部設備,對鼠標有效但禁止Windows顯示光標
  須要指出的第一點是,同一時刻不能有兩個程序或一個程序的兩個實例以獨佔模式得到同一設備。這主要是出於安全考慮,它能夠防止輸入給某一程序的值被傳給同時運行的另外一程序。對力反饋設備而言此時只能使用獨佔模式,上述這種特色能夠防止兩個程序同時輸出做用效果,而若是同時輸出則將產生混淆和警告。獨佔模式針對程序與系統間的相互做用不一樣而有多個分支:
  因爲Windows要求在任什麼時候刻以同於獨佔的模式訪問鍵盤,所以程序只能以非獨佔模式得到鍵盤。
  因爲Windows要求在任什麼時候刻以等同於獨佔的模式使用鼠標,因此當程序以獨佔模式得到鼠標時,Windows將不能訪問鼠標。這就意味着不會再產生任何鼠標消息,光標也將消失。注意這種表現與鍵盤不一樣,對鍵盤而言Windows有優先特權,因此重要的按鈕組合如ALT_TAB及CTRL_ALT_DELETE老是有效。  若是考慮其表現則沒有太多的理由優先選用獨佔模式。對鼠標而言選擇獨佔模式爲這樣可有一點點好處,因以使Windows再也不有產生鼠標消息的必要。但對遊戲杆而言,選擇哪一種模式對錶現沒有影響。之前臺模式得到設備意味着只有程序的主窗口位於前臺時才能接收輸入,對遊戲和多數其它程序而言這種設置是天然的。後臺模式則意味着只要程序在運行就能夠接收輸入,哪怕它已被最小化。想象一下一個「Smarthouse」程序,人們能夠單擊一個遠程控制來打開車庫門或開始燒烤食物,這種程序能夠一直停留在Windows任務欄上,而沒必要被帶到前臺進行工做,它是一個很好的後臺模式的例子。
  另外一種使用後臺模式的狀況是在當一對話框處於打開狀態同時要保持得到設備時。由於當屬性頁位於前臺時,用戶須要體察其效果,因此設備只能之後臺模式得到。若是已設置了前臺協做級別,則只要程序轉爲後臺,它將不能訪問所用的設備。對鼠標而言,當用戶打開菜單或對話框,甚至程序靜止在前臺時都將失去訪問權。這種狀況只能準備在設備從新有效時再次得到。下面開始講得到設備。
  1.4.7 得到設備
  到這裏爲止DirectInput設置已包含了簡單地準備設備待用的一勞永逸的步驟,但在能獲得數據前還必須得到該設備。得到設備便是取得它的使用權並通知DirectInput說明程序想按設定的格式接收數據。但得到設備不是一勞永逸的步驟,因爲種種緣由,程序可能屢次失去對設備的獲取,每當這種狀況發生時,程序須要從新得到所需設備,但DirectInput不會代勞。
  假設DirectInput是一家旅遊聖地豪華館餐廳的服務生,它知道顧客喜歡坐在什麼地方,是否願意和他人用同一張餐桌,喜好什麼樣的鮮花擺設等。但並非任什麼時候候顧客都能徑直走入餐廳並坐到本身喜歡的座位上,由於有可能另外一個團體已包用了他的餐桌,或者別人剛用完離去後,餐桌將來得及收拾和按喜歡的方式設置。所以,顧客應首先找服務生登記使各類安排都準備穩當。
  找服務生登記並獲准坐下和得到設備是同樣的。每次得到設備時,DirectInput必須把各方面設成用戶指定的方式,即清除全部緩衝區內的數據,以最有效的方式爲數據傳輸做安排,同時給出用戶選擇的那個設備的屬性。離開餐廳等同於調用IDirectInputDevice::Unacquire。其目的是讓DirectInput知道其它程序或Windows系統如今能夠以任何級別訪問該設備了。
  在結束程序前有時必須有意地釋放設備,最多見的狀況是當鼠標被以獨佔模式得到時Windows系統將不能使用鼠標,這在前面已講過。這時若是想讓Windows接管鼠標的使用權進行菜單選擇,就必須調用Unacquire以使Windows作它應做的事;當菜單關閉後再從新得到鼠標。DirectX SDK中的示例程序Scrawl提供了這種用法的例子,當用戶右擊鼠標時Scrawl釋放鼠標並打開一個上下文菜單,用戶能夠在標準的Windows鼠標光標下進行選擇。
  若是想改變除力反饋增益之外的設備屬性則也須要先釋放該設備。前面已提到過,程序有時會不自覺地釋放設備,發生這種狀況一般是因爲程序之前臺模式得到設備,但使用者已切換到另外一窗口,這就比如那位假想的Maitre d’被賄賂或受到威脅而離開了用戶的餐桌,固然可能這個類比不很恰當。
  不管釋放設備是有意的仍是不自覺的,在想從新獲取數據以前都必須通知DirectInput,這可由調用IDirectInputDevice::Acquire實現。但並無簡單快速的方法來檢驗設備是否處於未獲取狀態,所能做的只是檢驗申請接收數據時的迴應是什麼,IDirectInputDevice::GetDeviceState和IDirectInputDevice::GetDeviceData 方法在數據流被打斷後的第一次調用時會返回DIERR_INPUTLOST。若是沒有從新得到設備,接下來調用時會返回DIERR_NOTACQUIRED。收到DIERR_INPUTLOST後能夠試着從新得到該設備並再取一次數據,見下面的代碼段:
  getstate:
  hr = g_pMouse->GetDeviceState( sizeof( DIMOUSESTATE ), &dims );
  if ( hr == DIERR_INPUTLOST )
  {
  hr = g_pMouse->Acquire();
  if ( SUCCEEDED( hr ) ) goto getstate;
  }
  可是,想以一樣的方法響應DIERR_NOTAQUIERD或其它失敗消息值是不可取的。假設程序之前臺協做級別使用一種設備,當使用者切換到另外一程序的窗口時,此程序就會在每次想取得數據時收到DIERR_NOTACQUIRED消息,這時任何獲取該設備的嘗試都是對CPU週期的浪費,並且還有產生死循環的危險。這時應響應WM_ACTIVATE消息來從新獲取該設備,由於此消息代表程序又切換到前臺了。
  對輸入設備的訪問進行管理確實頗有好處,但這須要在適當的時候進行,當假想的人走近並開槍射擊時,若是用戶不能瘋狂地按鼠標或拍打遊戲杆,那麼一切就都沒有意義了。因此應總保持安全,試圖從新獲取已得到的設備不會有什麼害處;對Acquire的多餘調用不會有效果。
  1.五、取得輸入數據
  在講如何真正從設備獲取數據這個很是重要的問題以前,先來回顧一下在此以前應進行的步驟。
  建立設備。若是支持的設備不是鼠標或鍵盤,則還有須要獲得一個IDirectInputDevice接口,由於至少會用到Poll方法。
  設置協做級別。
  設置數據格式。
  設置設備屬性。對遊戲杆而言這一步是必需的,由於不設置軸的範圍就不能繼續進行後面的過程。若是想用緩衝區數據,則也須要設置其屬性,由於必須把緩衝區大小從0改成某個值。
  得到設備。與前面幾步不一樣,這一步不是一勞永逸的。   1.5.1 兩類數據
  若是讀者使用了舊風格的Windows輸入編程方法就須要爲諸如WM_KEYDOWN、WM_MOUSEMOVE等消息寫處理程序。這些消息由Windows產生以響應設備中單個發生的事件,如鍵或按鈕被按下或鬆開或者鼠標有了足夠的移動而產生了一箇中斷等。
  儘管DirectInput不使用Windows消息,但它採用了相似的取得輸入信息的系統,這個系統稱爲緩衝區數據。當設備的緩衝區大小屬性被設爲大於0的值之後,這一系統就開始運做。每當有輸入事件發生時,都會有一個數據包被建立,但這些數據包被放在一個私有緩衝區中,而不是轉換成消息的格式。在任什麼時候刻均可以用IDirectInputDevice::GetDeviceData方法從那些緩衝區中取出數據包。
  讀者或許也已用過相似於joyGetPosEx和GetAsyncKeyState函數,它們返回的不是離散的事件,而是整個設備或單個鍵的當前狀態的「快照」。在DirectInput中這被稱爲當即數據,它存在於數據包中,數據包的格式便是爲設備設置的格式,調用IDirectInputDevice::GetDeviceState方法能夠取得這些數據。
  讀者能夠在程序中任意選擇使用上述類型的數據,咱們的示例程序中使用當即數據來檢測箭頭鍵或遊戲杆軸的狀態,用緩衝區數據搜尋對發射鍵的敲擊。鼠標的移動也使用了緩衝區數據,由於咱們程序中的光標響應的是鼠標的移動而不是其位置。
  從緩衝區中清除一個條目並不會改變GetDeviceState返回數據,由於它返回的老是設備的當前真實狀態。就這一點而言它與標準的WindowsGetKeyboardState函數不一樣,後者實際上只是跟蹤被處理的鍵盤消息。第四、五、6 章有關鼠標、遊戲杆和鍵盤輸入將詳細說明當即數據和緩衝區數據。
  1.5.2 事件通知
  DirectInput提供一種機制以通知程序設備上是否發生某事或程序是否失去了設備,用Win32 CreatEvent函數建立一個事件,而後經過把其句柄傳遞給IDirectInputDevice::SetEventNotification方法(見表1-14)將其關聯到某個設備。
  表1-14 SetEventNotification方法
  HRESULT SetEventNotification(
  HANDLE hEvent
  );
  參數 說明
  CreateEvent 返回的事件句柄,當設備狀態改變時此事件將被設置。想關閉某設備的事件通知可經過傳NULL實現
  事件通知對於實時遊戲並不十分有用,這時通常採用的方法是在每次循環時檢測設備狀態或輸入數據緩衝區的內容,也能夠同時對這兩項進行檢測。從另外一方面說,程序在鼠標或鍵盤輸入以前不作任何有關的事能夠更有效地使用處理器,使之等待事件發生而不是總在輪詢各個設備(見DXSDK中的Srawl示例程序)。但對於其它設備,則不得不採用輪詢的方法,緣由稍後再說明,所以檢測信號並不比直接查詢設備狀態或緩衝區好多少。若是確實發現了使用事件通知的場合,那麼在SDK參考中有大量相似於Scrawl中IDirectInputDevice::SetEventNotification的示例代碼可使用。
  注意:本章和後續章節中,咱們常用「事件」一詞來講明設備狀態或緩衝區裏數據項發生改變的事情,如按壓按鈕或軸的變化等,通常它不表示本章中咱們講到的事件對象,在使用狹義時咱們將盡可能表達清楚。
  1.5.3 輪詢以獲取數據
  當設備狀態改變時DirectInput是如何知道的呢?某些狀況下,它經過舊風格的方法即硬件中斷實現,不然就必須查詢驅動器以得到當前設備狀態並用一些小把戲來模擬事件(廣義和狹義)。全部這些都在幕後靜靜地運行,可是首先須要作一件事以使輪詢操做發生。DirectInput不會自動輪詢設備,而只在調用IDirectInputDevice::Poll方法時才這麼作。該方法沒有任何參數。
  調用Poll後會發生什麼呢?DirectInput先檢查設備是否確實須要輪詢。若是不須要 — 即若是DirectInput經過硬件中斷獲知狀態變化 — 則Poll方法當即返回。不然DirectInput檢查設備的狀態並取得當即數據。若是已爲緩衝區數據留出了空間,則DirectInput會把這次設備的狀態與上次調用時的狀態相比較,並根據發現的變化建立數據包,而後它將數據包存入緩衝區中,就好象這些數據是從中斷獲得的同樣,接下來會針對已建立的事件發出信號。
  能夠經過檢測DIDEVCAPS結構的DIDC_POLLEDDATAFORMAT標誌來驗證是否設備上的某部分須要輪詢。也能夠經過檢測DIDEVICEOBJECTINSTANCE中的DIDOI_POLLED以獲知是否設備上的特定設備物須要輪詢。可是更安全經濟的作法是無論有無必要都調用Poll,若是設備不須要輪詢,則該方法幾乎當即就返回。
  總之,在每次IDirectInputDevice::GetDeviceState或IdirectInputDevice2::GetDeviceData獲取數據以前最好都調用IdirectInputDevice2::Poll,除非確知程序只用到中斷驅動的設備。
  提示:若是程序在消息循環中尋找有標誌的輸入事件,則能夠調用WaitForSingleObject、WaitForMultipleObjects或其它的一種檢測事件的函數,並把超時值(dwMilliseconds)設爲0,而後若是該函數返回後未找到事件對象,就調用Poll。在SDKIDirectInputDevice::SetEventNotification參考的最後一個例子中,就能夠在DoGame函數內調用Poll。編程

相關文章
相關標籤/搜索