前言
提供鍵鼠輸入可以說是一個游戲的必備要素。在這里,我們不使用DirectInput
,而是使用Windows消息處理機制中的Raw Input
,不過要從頭開始實現會讓事情變得很復雜。DXTK提供了鼠標輸入的Mouse.h
和鍵盤輸入的Keyboard.h
(現在已經單獨抽離出來使用),對消息處理機制進行了封裝,使用Mouse
類和Keyboard
類可以讓我們的開發效率事半功倍。
對Raw Input
有興趣的同學,你可以看鍵鼠類的內部實現,也可以看MSDN文檔: Raw Input
Mouse
類和Keyboard
類都在名稱空間DirectX
內。
DirectX11 With Windows SDK完整目錄
歡迎加入QQ群: 727623616 可以一起探討DX11,以及有什么問題也可以在這里匯報。
鼠標輸入
Mouse
類是一個單例類,我們可以通過Mouse::Get()
靜態方法來獲取該實例:
static Mouse& Mouse::Get();
也可以在應用程序類中直接作為成員使用,因為它提供了默認構造函數。不過為了異常安全,建議使用智能指針:
std::unique_ptr<DirectX::Mouse> m_pMouse;
m_pMouse = std::make_unique<DirectX::Mouse>();
要想使用鼠標類,我們還需要完成三件事情:
1.在初始化階段給鼠標類設置要綁定的窗口句柄,使用Mouse::SetWindow
方法:
void Mouse::SetWindow(HWND window);
2.設置鼠標模式,需要使用Mouse::SetMode
方法:
void Mouse::SetMode(Mouse::Mode mode);
鼠標模式有下面兩種:
enum Mode
{
MODE_ABSOLUTE = 0, // 絕對坐標模式,每次狀態更新xy值為屏幕像素坐標,且鼠標可見
MODE_RELATIVE, // 相對運動模式,每次狀態更新xy值為每一幀之間的像素位移量,且鼠標不可見
};
可以加在D3DApp::Init
方法上:
bool D3DApp::Init()
{
m_pMouse = std::make_unique<DirectX::Mouse>();
m_pKeyboard = std::make_unique<DirectX::Keyboard>();
if (!InitMainWindow())
return false;
if (!InitDirect3D())
return false;
return true;
}
3.在消息處理的回調函數上進行修改,在接收到下面這些消息后,需要調用Mouse::ProcessMessage
方法:
WM_ACTIVATEAPP WM_INPUT
WM_LBUTTONDOWN WM_MBUTTONDOWN WM_RBUTTONDOWN WM_XBUTTONDOWN
WM_LBUTTONUP WM_MBUTTONUP WM_RBUTTONUP WM_XBUTTONUP
WM_MOUSEWHEEL WM_MOUSEHOVER WM_MOUSEMOVE
具體的修改的部分在最后會展示。
完成這些操作后,我們的程序就可以開始使用鼠標了。
對於每一幀,我們可以通過Mouse::GetState
方法獲取當前幀下鼠標的運動狀態:
Mouse::State Mouse::GetState() const;
Mouse::State
包含如下成員:
struct State
{
bool leftButton; // 鼠標左鍵被按下
bool middleButton; // 鼠標滾輪鍵被按下
bool rightButton; // 鼠標右鍵被按下
bool xButton1; // 忽略
bool xButton2; // 忽略
int x; // 絕對坐標x或相對偏移量
int y; // 絕對坐標y或相對偏移量
int scrollWheelValue; // 滾輪滾動累積值
Mode positionMode; // 鼠標模式
};
對於剩下的方法:
Mouse::ResetScrollWheelValue
方法可以清空滾輪的滾動累積值
Mouse::IsConnected
方法則可以檢驗鼠標是否連接
Mouse::SetVisible
方法設置鼠標是否可見
Mouse::IsVisible
方法可以檢驗鼠標是否可見
鼠標狀態追蹤
Mouse::ButtonStateTracker
類提供了更高級的功能,通過根據上一次的鼠標事件和當前鼠標事件的對比,來判斷鼠標的狀態。它有兩個重要方法:
void Mouse::ButtonStateTracker::Update( const Mouse::State& state );
// 在每一幀的時候應提供Mouse的當前狀態去更新它
State Mouse::ButtonStateTracker::GetLastState() const;
// 獲取上一幀的鼠標事件,應當在Update之前使用,否則變為獲取當前幀的狀態
然后還有5個重要公共成員:
ButtonState leftButton; // 鼠標左鍵狀態
ButtonState middleButton; // 鼠標滾輪按鍵狀態
ButtonState rightButton; // 鼠標右鍵狀態
ButtonState xButton1; // 忽略
ButtonState xButton2; // 忽略
注意: 這里要區分
State
和ButtonState
類型的五個同名成員,含義不同。
枚舉量ButtonState
含義:
enum ButtonState
{
UP = 0, // 按鈕未被按下
HELD = 1, // 按鈕長按中
RELEASED = 2, // 按鈕剛被放開
PRESSED = 3, // 按鈕剛被按下
};
由於鼠標狀態追蹤類不是單例,而且它存有上一幀的鼠標狀態,不應該作為一個臨時變量,而是也應該像鼠標類一樣在整個程序生命周期內都存在。
在絕對模式下,我們也可以獲取兩幀之間的鼠標相對位移量:
Mouse::State mouseState = m_pMouse->GetState();
Mouse::State lastMouseState = mMouseTracker.GetLastState();
int dx = mouseState.x - lastMouseState.x, dy = mouseState.y - lastMouseState.y;
雖然這樣子第一幀的時候,鼠標狀態追蹤類並沒有開始記錄,而鼠標已經記錄下了第一幀的狀態,但由於開始運行的第一幀通常都很快,等到我們第一次使用鼠標的時候已經經過了一段時間,所以並不會產生什么問題。
然后在更新了跟蹤器狀態后,就可以判斷鼠標狀態做進一步操作了。以渲染立方體的項目為例,這里打算通過鼠標拖動產生旋轉,如:
// 更新鼠標按鈕狀態跟蹤器,僅當鼠標按住的情況下才進行移動
mMouseTracker.Update(state);
if (mouseState.leftButton == true && mMouseTracker.leftButton == mMouseTracker.HELD)
{
// 旋轉立方體
cubeTheta -= (mouseState.x - lastMouseState.x) * 0.01f;
cubePhi -= (mouseState.y - lastMouseState.y) * 0.01f;
}
mCBuffer.world = XMMatrixRotationY(cubeTheta) * XMMatrixRotationX(cubePhi);
鍵盤輸入
Keyboard
類也是一個單例類,我們可以通過Keyboard::Get()
靜態方法來獲取該實例:
static Keyboard& Keyboard::Get();
也可以在應用程序類中直接作為成員使用,因為它提供了默認構造函數。不過為了異常安全,建議使用智能指針:
std::unique_ptr<DirectX::Keyboard> m_pKeyboard;
m_pKeyboard = std::make_unique<DirectX::Keyboard>();
要想使用鍵盤類,我們還需要完成一件或兩件事情:
1.如果Keyboard
類內有SetWindow
方法,則需要調用以初始化,否則不需要:
void Keyboard::SetWindow(ABI::Windows::UI::Core::ICoreWindow* window);
2.在消息處理的回調函數上進行修改,在接收到下面這些消息后,需要調用Keyboard::ProcessMessage
方法:
WM_ACTIVATEAPP
WM_KEYDOWN WM_SYSKEYDOWN WM_KEYUP WM_SYSKEYUP
具體的修改的部分在最后會展示。
完成上述操作我們就可以使用鍵盤了。
對於每一幀,我們可以通過Keyboard::GetState
方法獲取當前幀下鍵盤所有按鍵的狀態:
Keyboard::State Keyboard::GetState() const;
Keyboard::State
結構體記錄了按鍵信息,而Keyboard::Keys
枚舉量定義了有哪些按鍵。獲取了鍵盤按鍵狀態后,我們要關注的是Keyboard::State
內的方法:
Keyboard::State::IsKeyDown
方法判斷按鍵是否被按下
bool Keyboard::State::IsKeyDown(Keyboard::Keys key) const;
Keyboard::State::IsKeyUp
方法判斷按鍵是否沒有按下
bool Keyboard::State::IsKeyUp(Keyboard::Keys key) const;
下面演示的是鍵盤連續操作,其中dt是兩幀之間的時間間隔
if (keyState.IsKeyDown(Keyboard::W))
cubePhi += dt * 2;
if (keyState.IsKeyDown(Keyboard::S))
cubePhi -= dt * 2;
if (keyState.IsKeyDown(Keyboard::A))
cubeTheta += dt * 2;
if (keyState.IsKeyDown(Keyboard::D))
cubeTheta -= dt * 2;
mCBuffer.world = XMMatrixRotationY(cubeTheta) * XMMatrixRotationX(cubePhi);
注意:對於鼠標和鍵盤的拖動距離,推薦鼠標用偏移量,而推薦鍵盤用按壓持續時間
鍵盤狀態追蹤
如果要判斷按鍵是剛按下還是剛放開,則需要Keyboard::KeyboardStateTracker
類幫助
Keyboard::KeyboardStateTracker::Update
方法需要接受當前幀的State以進行更新:
void Keyboard::KeyboardStateTracker::Update(const Keyboard::State& state);
然后就可以使用它的IsKeyPressed
或IsKeyReleased
方法來進行判斷鍵盤按鍵是否剛按下,或者剛釋放了,同樣需要接受Keyboard::Keys
枚舉量
於是我們可以嘗試讓前面的立方體通過鍵盤或鼠標動起來。
D3DApp::MsgProc方法的變化
這里展示了消息處理部分的變化:
LRESULT D3DApp::MsgProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
switch (msg)
{
// 省略原有的部分...
// 監測這些鍵盤/鼠標事件
case WM_INPUT:
case WM_LBUTTONDOWN:
case WM_MBUTTONDOWN:
case WM_RBUTTONDOWN:
case WM_XBUTTONDOWN:
case WM_LBUTTONUP:
case WM_MBUTTONUP:
case WM_RBUTTONUP:
case WM_XBUTTONUP:
case WM_MOUSEWHEEL:
case WM_MOUSEHOVER:
case WM_MOUSEMOVE:
m_pMouse->ProcessMessage(msg, wParam, lParam);
return 0;
case WM_KEYDOWN:
case WM_SYSKEYDOWN:
case WM_KEYUP:
case WM_SYSKEYUP:
m_pKeyboard->ProcessMessage(msg, wParam, lParam);
return 0;
case WM_ACTIVATEAPP:
m_pMouse->ProcessMessage(msg, wParam, lParam);
m_pKeyboard->ProcessMessage(msg, wParam, lParam);
return 0;
}
return DefWindowProc(hwnd, msg, wParam, lParam);
}
補充說明:當你打開Mouse.cpp
和Keyboard.cpp
查看源碼的時候,大概率會遇到下面的報錯:
你可以直接無視該錯誤繼續編譯,即便到VS2019這個情況依然存在。
現在來看看當前章節對應的項目。該程序使用鍵盤的WSAD四個鍵控制立方體旋轉,或者鼠標拖動旋轉。
DirectX11 With Windows SDK完整目錄
歡迎加入QQ群: 727623616 可以一起探討DX11,以及有什么問題也可以在這里匯報。