Device Input

Device Input

用于交互渲染的另一种支持系统是,响应输入设备的能力。在PC端,输入设备主要包括鼠标,键盘以及游戏手柄。本小节主要讨论鼠标和键盘输入,并创建相应的game components。
获取设备的输入信息通常有两种方法:一种是通过定时轮询设备,另一种是等待设备在状态发生改变时的通知。具体选择使用什么方法,需要考虑输入状态发生改变的频率,轮询设备所需的成本,以及处理一个事件的成本。比如,如果设备状态在每一帧都会发生改,通过轮询的方法比处理大量的事件要好;相反,如果设备大部分时间都处于空闲状态,只会定期的发生改变,最好是使用一个基于事件的输入系统。但是,并不需要一种适用于所有方法的技术,可以使用轮询的方法专门处理一种设备,使用事件方法处理其他的设备。
有多种API用于响应设备的输入。例如,在Windows API可以通过windows procedure(在Game类中编写的WndProc()函数)处理鼠标和键盘事件或者通过GetAsyncKeyState()函数轮询设备。DirectX 11则包含了获取Xbox 360游戏控制器输入信息的 XInpu系统,但是XInput不支持鼠标和键盘输入。DirectX 11安装包中还包含了以前的 DirectInput库。DirectInput支持鼠标,键盘,游戏手柄和摇杆输入,但是自从DirectX 8之后就再也没有更新了。Microsoft建议在“下一代”游戏控制器中使用XInput,但是自从DirectX 9之后XInput也没有重要的更新。

在本书中所有的输入components,主要使用DirectInput库轮询键盘和鼠标设备。

Keyboard Input

使用DirectInput查询键盘的操作可以转化为以下几个步骤:
1.创建DirectInput对象。
2.创建DirectInput键盘device。
3.设置device的数据格式和cooperative level。
4.获取device。
5.查询device状态。
下面每一小节对应讲解一个步骤。

Create the DirectInput Object

IDirectInput8接口支持枚举,创建以及查询DirectInput设备。在执行这个操作之前,需要先创建一个IDirectInput8对象。通过DirectInput8Create()函数可以创建该对象,dinput.h头文件中声明该函数以及所有与DirectInput相关的功能,函数原型和参数如下:

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


hinst:指定创建DirectInput对象的应用程序的句柄。与用于窗口初始化中的句柄是同一个,存储在Game类的成员变量mInstance中。
dwVersion:DirectInput的版本号,总是赋值为DIRECTINPUT_VERSION。
riidltf:用于获取IDirectInput8接口的唯一标识符(unique identifier),总是设为IID_IDirectInput8。
ppvOut:返回创建成功的DirectInput对象。
punkOuter:用于COM聚合,总是设为NULL。
创建完DirectInput对象之后,鼠标和键盘components就可以使用该对象了,然后在删除components后就可以释放该对象了。因此,需要在RenderingGame类中增加一个表示DirectInput对象的成员变量,然后在Initialize()函数中初始化该变量,在Shutdown()函数释放。列表12.12列出了RenderingGame实现代码中调用DirectInput8Create()函数,以及释放该对象的示例。
列表12.12 DirectInput Object Creation and Release

void RenderingGame::Initialize()
{
	if (FAILED(DirectInput8Create(mInstance, DIRECTINPUT_VERSION,
		IID_IDirectInput8, (LPVOID*)&mDirectInput, nullptr)))
	{
		throw GameException("DirectInput8Create() failed");
	}
	/* ... Previously presented statements removed for brevity ... */
}
void RenderingGame::Shutdown()
{
	/* ... Previously presented statements removed for brevity ... */
	ReleaseObject(mDirectInput);
	Game::Shutdown();
}


Create the DirectInput Keyboard Device

初始化DirectInput对象之后,就可以创建具体的device了。首先在Library工程中添加一个命名为Keyboard的类,该类继承自GameComponent类,并添加了创建,设置,获取,释放以及查询键盘的函数。在类中添加一个IDirectInputDevice8接口类型的成员变量,用于存储创建的device。通过调用IDirectInput8::CreateDevice()函数创建device,该函数的原型和参数如下:

HRESULT CreateDevice(
	REFGUID rguid,
	LPDIRECTINPUTDEVICE * lplpDirectInputDevice,
	LPUNKNOWN pUnkOuter);


rguid:所请求的输入设备的唯一标识符。比如,创建键盘设备,值为GUID_SysKeyboard。
lplpDirectInputDevice:返回创建成功的DirectInput设备。
punkOuter:用于COM聚合,总是设为NULL。
创建,设置以及获取一个输入设备通常使用相同的函数调用代码。在讨论这部分主题时,再讲解示例。

Set the Device Data Format and Cooperative Level

创建输入devcie之后,需要指定device返回的数据格式。通过调用IDirectInputDevice8::SetDataFormat()函数就可以指定数据格式,该函数只有一个参数:一个DIDATAFORMAT结构体类型的变量。可以设定一个自定义的DIDATAFORMAT结构体或者使用下列预定义实例的某一个:
c_dfDIKeyboard
c_dfDIMouse
c_dfDIMouse2
c_dfDIJoystick
c_dfDIJoystick2

对于keyboard component,应该使用c_dfDIKeyboard。

接下来,需要设置cooperative level,用于描述该设备实例与同一个设备的其他实例对象的交互方式,以及与操作系统中其他设备的实例对象的交互。通过调用IDirectInputDevice8::SetCooperativeLevel()函数完成cooperatiove level的设置,该函数原型和参数为:

HRESULT SetCooperativeLevel(
	HWND hwnd,
	DWORD dwFlags);

hwnd:应用程序中位于最顶层的窗口句柄。由Game::InitializeWindow()函数创建,并存储在Game::mWindowHandle成员变量中。
dwFlags:由一些枚举常量按位于运算得到,用于描述cooperative level。表格12.1列出了相关的枚举常量值。


表格12.1 DirectInput Device Cooperative-Level Flags

Acquire the Device

在完成了data foramt和cooperative level的设置之后,就可以获取deivce了。成功获取一个device,意味着可以访问device并查询device的状态。通过调用IDirectInputDevcie8::Acquire()函数可以获取device,该函数没有输入参数。列表12.13列出了Keyboard::Initialize()函数的实现代码,演示了这几节所描述的函数调用。
列表12.13 The Keyboard::Initialize() Method

void Keyboard::Initialize()
{
	if (FAILED(mDirectInput->CreateDevice(GUID_SysKeyboard, &mDevice, nullptr)))
	{
		throw GameException("IDIRECTINPUT8::CreateDevice() failed");
	}

	if (FAILED(mDevice->SetDataFormat(&c_dfDIKeyboard)))
	{
		throw GameException("IDIRECTINPUTDEVICE8::SetDataFormat() failed");
	}

	if (FAILED(mDevice->SetCooperativeLevel(mGame->WindowHandle(), DISCL_FOREGROUND | DISCL_NONEXCLUSIVE)))
	{
		throw GameException("IDIRECTINPUTDEVICE8::SetCooperativeLevel() failed");
	}

	mDevice->Acquire();
}


Query the Device State

现在,已经完成了device的初始化,并成功获取了device,接下来就可以查询devcie的状态了。查询状态是通过调用IDirectInputDevice8::GetDeviceState()函数,该函数的原型和参数如下:

HRESULT GetDeviceState(
	DWORD cbData,
	LPVOID lpvData);


cbData:参数 lpvData所指定buffer的大小(以bytes为单位)。
lpvData:用于保存返回的device状态的buffer。该buffer的数据结构由IDirectInputDevice8::SetDataFormat()函数指定的数据格式确定。当使用预定义的c_dfIDKeyboard数据格式时,buffer就是一个简单的256bytes的数组。

A Keyboard Component

列表12.14和12.15中分别列出了Keyboard类的完整声明代码,和实现代码,而不是仅仅列出IDirectInputDevice8::GetDeviceState()函数调用的代码。该函数的调用代码包含在Keyboard::Update()函数中。
列表12.14 The Keyboard.h Header File

#pragma once

#include "GameComponent.h"

namespace Library
{
    class Keyboard : public GameComponent
    {
        RTTI_DECLARATIONS(Keyboard, GameComponent)

    public:
        Keyboard(Game& game, LPDIRECTINPUT8 directInput);
        ~Keyboard();

        const byte* const CurrentState() const;
        const byte* const LastState() const;

        virtual void Initialize() override;
        virtual void Update(const GameTime& gameTime) override;
        
        bool IsKeyUp(byte key) const;
        bool IsKeyDown(byte key) const;
        bool WasKeyUp(byte key) const;
        bool WasKeyDown(byte key) const;
        bool WasKeyPressedThisFrame(byte key) const;
        bool WasKeyReleasedThisFrame(byte key) const;
        bool IsKeyHeldDown(byte key) const;

    private:
        Keyboard();

        static const int KeyCount = 256;

        Keyboard(const Keyboard& rhs);

        LPDIRECTINPUT8 mDirectInput;
        LPDIRECTINPUTDEVICE8 mDevice;
        byte mCurrentState[KeyCount];
        byte mLastState[KeyCount];
    };
}


列表12.15 The Keyboard.cpp File

#include "Keyboard.h"
#include "Game.h"
#include "GameTime.h"
#include "GameException.h"

namespace Library
{
	RTTI_DEFINITIONS(Keyboard)

	Keyboard::Keyboard(Game& game, LPDIRECTINPUT8 directInput)
		: GameComponent(game), mDirectInput(directInput), mDevice(nullptr)
	{
		assert(mDirectInput != nullptr);		
		ZeroMemory(mCurrentState, sizeof(mCurrentState));
		ZeroMemory(mLastState, sizeof(mLastState));
	}

	Keyboard::~Keyboard()
	{
		if (mDevice != nullptr)
		{
			mDevice->Unacquire();
			mDevice->Release();
			mDevice = nullptr;
		}
	}

	const byte* const Keyboard::CurrentState() const
	{
		return mCurrentState;
	}

	const byte* const Keyboard::LastState() const
	{
		return mLastState;
	}

	void Keyboard::Initialize()
	{
		if (FAILED(mDirectInput->CreateDevice(GUID_SysKeyboard, &mDevice, nullptr)))
		{
			throw GameException("IDIRECTINPUT8::CreateDevice() failed");
		}

		if (FAILED(mDevice->SetDataFormat(&c_dfDIKeyboard)))
		{
			throw GameException("IDIRECTINPUTDEVICE8::SetDataFormat() failed");
		}

		if (FAILED(mDevice->SetCooperativeLevel(mGame->WindowHandle(), DISCL_FOREGROUND| DISCL_NONEXCLUSIVE)))
		{
			throw GameException("IDIRECTINPUTDEVICE8::SetCooperativeLevel() failed");
		}

		mDevice->Acquire();
	}

	void Keyboard::Update(const GameTime& gameTime)
	{
		if (mDevice != nullptr)
		{
			memcpy(mLastState, mCurrentState, sizeof(mCurrentState));

			if (FAILED(mDevice->GetDeviceState(sizeof(mCurrentState), (LPVOID)mCurrentState)))
			{
				// Try to reaqcuire the device
				if (SUCCEEDED(mDevice->Acquire()))
				{
					mDevice->GetDeviceState(sizeof(mCurrentState), (LPVOID)mCurrentState);
				}				
			}
		}
	}

	bool Keyboard::IsKeyUp(byte key) const
	{
		return ((mCurrentState[key] & 0x80) == 0);
	}

	bool Keyboard::IsKeyDown(byte key) const
	{
		return ((mCurrentState[key] & 0x80) != 0);
	}

	bool Keyboard::WasKeyUp(byte key) const
	{
		return ((mLastState[key] & 0x80) == 0);
	}

	bool Keyboard::WasKeyDown(byte key) const
	{
		return ((mLastState[key] & 0x80) != 0);
	}

	bool Keyboard::WasKeyPressedThisFrame(byte key) const
	{
		return (IsKeyDown(key) && WasKeyUp(key));
	}

	bool Keyboard::WasKeyReleasedThisFrame(byte key) const
	{
		return (IsKeyUp(key) && WasKeyDown(key));
	}

	bool Keyboard::IsKeyHeldDown(byte key) const
	{
		return (IsKeyDown(key) && WasKeyDown(key));
	}
}


Keyboard类的实现与前面创建game component的方式一样。在Initialize()函数中创建,设置并获取输入设备,然后在每一次调用Update()函数时获取device的状态。在Keyboard中声明了两个成员变量mCurrentState和mLastState,分别用于表示device在当前帧中的状态,以及上一帧中的状态。这样就可以通过WasKeyPressedThisFrame()函数,WasKeyReleaseThisFrame()函数或者IsKeyHeldDown()函数判断某个按键的相关状态。这三个函数的参数都是byte类型(unsigned char)的键盘码数组的索引值。DirectInput库包含一组用于查询按键的定义,包含在dinput.h头文件中,所有的键盘扫描码都以DIK_为前缀。表格12.2列出这些扫描码中的一部分。


表格12.2 Common DirectInput Keyboard Scan Codes

Reacquiring the Device

列表12.15所示的Keyboard::Update()函数中有一个额外的调用IDirectInputDevice8::Acquire()函数的操作。因为在程序执行的过程中,可能会失去device的访问权,所以需要在查询device之前要重新获取访问权。例如,当另外一个应用程序请求独占该device时,就会发生这种情况。在Keyboard::Update()函数中,如果GetDeviceState()函数调用失败,只是简单重新获取device。并没有判断如果重新获取device还是失败,需要向用户提示错误或警告。如果需要这种提醒功能,可以自己实现。

Integrating the Keyboard Component

在上节中已经在RenderingGame类中集成了frame rate component,现在以同样的方式集成keyboard component。首先在RenderingGame类中增加一个Keyboard类型的成员变量,然后如12.16所示修改RenderingGame::Initialize()函数和RenderingGame::Shutdown()函数。列表中还包含了RenderingGame::Update()函数的代码,其中在按下Escape按键时就会退出程序。
列表12.16 Integration of the Keyboard Component Within the RenderingGame Class

void RenderingGame::Initialize()
{
	if (FAILED(DirectInput8Create(mInstance, DIRECTINPUT_VERSION, IID_IDirectInput8, (LPVOID*)&mDirectInput, nullptr)))
	{
		throw GameException("DirectInput8Create() failed");
	}

	mKeyboard = new Keyboard(*this, mDirectInput);
	mComponents.push_back(mKeyboard);
	mServices.AddService(Keyboard::TypeIdClass(), mKeyboard);

	mMouse = new Mouse(*this, mDirectInput);
	mComponents.push_back(mMouse);
	mServices.AddService(Mouse::TypeIdClass(), mMouse);

	mFpsComponent = new FpsComponent(*this);
	mComponents.push_back(mFpsComponent);

	SetCurrentDirectory(Utility::ExecutableDirectory().c_str());

	mSpriteBatch = new SpriteBatch(mDirect3DDeviceContext);
	mSpriteFont = new SpriteFont(mDirect3DDevice, L"Content\\Fonts\\Arial_14_Regular.spritefont");

	Game::Initialize();
}

void RenderingGame::Shutdown()
{
	DeleteObject(mKeyboard);
	DeleteObject(mMouse);
	DeleteObject(mFpsComponent);
	DeleteObject(mSpriteFont);
	DeleteObject(mSpriteBatch);

	ReleaseObject(mDirectInput);

	Game::Shutdown();
}

void RenderingGame::Update(const GameTime &gameTime)
{
	if (mKeyboard->WasKeyPressedThisFrame(DIK_ESCAPE))
	{
		Exit();
	}

	Game::Update(gameTime);
}

Mouse Input

使用DirectInput访问鼠标,与获取键盘所使用的接口和函数基本相同。主要的区别是要查询设备的数据不同,在Keyboard类中mCurrentState和mLastState成员变量表示键盘扫描码,而在Mouse类中这两个变量表示DIMOUSESTATE结构类型实例。
DIMOUSESTATE结构的定义如下:
typedef struct _DIMOUSESTATE {
	LONG lX;
	LONG lY;
	LONG lZ;
	BYTE rgbButtons[4];
} DIMOUSESTATE;


其中,成员lX和lY分别存储鼠标的X和Y坐标值,lZ表示鼠标滚轮的位置。需要注意的是,这些坐标值表示的是相对于鼠标移动之前的变化值。比如,X为负值表示鼠标向左移动了,Y为正值则表示鼠标向下移动了(朝向屏幕的底部移动)。这些值的数量越大,表示鼠标相对移动的位置越远。
另一个成员rgbButtons数组,支持带有4个按键的鼠标,数组中的每个成员都由下的枚举常量表示:
enum MouseButtons
{
	MouseButtonsLeft = 0,
	MouseButtonsRight = 1,
	MouseButtonsMiddle = 2,
	MouseButtonsX1 = 3
};


对于鼠标device的数据格式,需要在调用IDirectInputDevice8::SetDataFormat()函数时指定c_dfDIMouse值。另外,还要设置IDirectInputDeive8::CreateDevice()的第一个参数为GUID_SysMouse,不再是GUID_SysKeyboard。
列表12.17和12.18列出了Mouse类的声明代码和实现代码。
列表12.17 The Mouse.h Header File

#pragma once

#include "GameComponent.h"

namespace Library
{
    class GameTime;

    enum MouseButtons
    {
        MouseButtonsLeft = 0,
        MouseButtonsRight = 1,
        MouseButtonsMiddle = 2,
        MouseButtonsX1 = 3
    };

    class Mouse : public GameComponent
    {
        RTTI_DECLARATIONS(Mouse, GameComponent)

    public:
        Mouse(Game& game, LPDIRECTINPUT8 directInput);
        ~Mouse();

        LPDIMOUSESTATE CurrentState();
        LPDIMOUSESTATE LastState();

        virtual void Initialize() override;
        virtual void Update(const GameTime& gameTime) override;
        
        long X() const;
        long Y() const;
        long Wheel() const;

        bool IsButtonUp(MouseButtons button) const;
        bool IsButtonDown(MouseButtons button) const;		
        bool WasButtonUp(MouseButtons button) const;
        bool WasButtonDown(MouseButtons button) const;	
        bool WasButtonPressedThisFrame(MouseButtons button) const;
        bool WasButtonReleasedThisFrame(MouseButtons button) const;
        bool IsButtonHeldDown(MouseButtons button) const;

    private:
        Mouse();

        LPDIRECTINPUT8 mDirectInput;
        LPDIRECTINPUTDEVICE8 mDevice;
        DIMOUSESTATE mCurrentState;
        DIMOUSESTATE mLastState;

        long mX;
        long mY;
        long mWheel;
    };
}

列表12.18 The Mouse.cpp File

#include "Mouse.h"
#include "Game.h"
#include "GameTime.h"
#include "GameException.h"

namespace Library
{
	RTTI_DEFINITIONS(Mouse)

	Mouse::Mouse(Game& game, LPDIRECTINPUT8 directInput)
		: GameComponent(game), mDirectInput(directInput), mDevice(nullptr), mX(0), mY(0), mWheel(0)
	{
		assert(mDirectInput != nullptr);		
		ZeroMemory(&mCurrentState, sizeof(mCurrentState));
		ZeroMemory(&mLastState, sizeof(mLastState));
	}

	Mouse::~Mouse()
	{
		if (mDevice != nullptr)
		{
			mDevice->Unacquire();
			mDevice->Release();
			mDevice = nullptr;
		}
	}

	LPDIMOUSESTATE Mouse::CurrentState()
	{
		return &mCurrentState;
	}

	LPDIMOUSESTATE Mouse::LastState()
	{
		return &mLastState;
	}

	long Mouse::X() const
	{
		return mX;
	}

	long Mouse::Y() const
	{
		return mY;
	}

	long Mouse::Wheel() const
	{
		return mWheel;
	}

	void Mouse::Initialize()
	{
		if (FAILED(mDirectInput->CreateDevice(GUID_SysMouse, &mDevice, nullptr)))
		{
			throw GameException("IDIRECTINPUT8::CreateDevice() failed");
		}

		if (FAILED(mDevice->SetDataFormat(&c_dfDIMouse)))
		{
			throw GameException("IDIRECTINPUTDEVICE8::SetDataFormat() failed");
		}

		if (FAILED(mDevice->SetCooperativeLevel(mGame->WindowHandle(), DISCL_FOREGROUND | DISCL_NONEXCLUSIVE)))
		{
			throw GameException("IDIRECTINPUTDEVICE8::SetCooperativeLevel() failed");
		}

		mDevice->Acquire();
	}

	void Mouse::Update(const GameTime& gameTime)
	{
		if (mDevice != nullptr)
		{
			memcpy(&mLastState, &mCurrentState, sizeof(mCurrentState));

			if (FAILED(mDevice->GetDeviceState(sizeof(mCurrentState), &mCurrentState)))
			{
				// Try to reaqcuire the device
				if (SUCCEEDED(mDevice->Acquire()))
				{
					if (FAILED(mDevice->GetDeviceState(sizeof(mCurrentState), &mCurrentState)))
					{
						return;
					}	
				}
			}

			// Accumulate positions
			mX += mCurrentState.lX;
			mY += mCurrentState.lY;
			mWheel += mCurrentState.lZ;
		}
	}

	bool Mouse::IsButtonUp(MouseButtons button) const
	{
		return ((mCurrentState.rgbButtons[button] & 0x80) == 0);
	}

	bool Mouse::IsButtonDown(MouseButtons button) const
	{
		return ((mCurrentState.rgbButtons[button] & 0x80) != 0);
	}

	bool Mouse::WasButtonUp(MouseButtons button) const
	{
		return ((mLastState.rgbButtons[button] & 0x80) == 0);
	}

	bool Mouse::WasButtonDown(MouseButtons button) const
	{
		return ((mLastState.rgbButtons[button] & 0x80) != 0);
	}

	bool Mouse::WasButtonPressedThisFrame(MouseButtons button) const
	{
		return (IsButtonDown(button) && WasButtonUp(button));
	}

	bool Mouse::WasButtonReleasedThisFrame(MouseButtons button) const
	{
		return (IsButtonUp(button) && WasButtonDown(button));
	}

	bool Mouse::IsButtonHeldDown(MouseButtons button) const
	{
		return (IsButtonDown(button) && WasButtonDown(button));
	}
}


Integrating the Mouse Component

列表12.19列出了在RenderingGame类中集成Mouse component的代码。该代码中修改了RenderingGame::Draw()函数,通过使用SpriteBatch/SpriteFont系统显示鼠标的位置和滚轮值。需要注意的是,鼠标的坐标值是通过把IDirectInputDevice8::GetDeviceState()函数所返回的相对值进行累加得到的,没有建立一个坐标原点,也没有把坐标值限制到应用程序的窗口内或屏幕内,这将作为一个练习留给读者。
列表12.19 Integration of the Mouse Component Within the RenderingGame Class(Initialize()和Shutdown()函数见列表12.16)

void RenderingGame::Draw(const GameTime &gameTime)
{
	mDirect3DDeviceContext->ClearRenderTargetView(mRenderTargetView, reinterpret_cast<const float*>(&BackgroundColor));
	mDirect3DDeviceContext->ClearDepthStencilView(mDepthStencilView, D3D11_CLEAR_DEPTH | D3D11_CLEAR_STENCIL, 1.0f, 0);

	Game::Draw(gameTime);

	mSpriteBatch->Begin();

	std::wostringstream mouseLabel;
	mouseLabel << L"Mouse Position: " << mMouse->X() << ", " << mMouse->Y() << "  Mouse Wheel: " << mMouse->Wheel();
	mSpriteFont->DrawString(mSpriteBatch, mouseLabel.str().c_str(), mMouseTextPosition);

	mSpriteBatch->End();

	HRESULT hr = mSwapChain->Present(0, 0);
	if (FAILED(hr))
	{
		throw GameException("IDXGISwapChain::Present() failed.", hr);
	}
}


图12.4显示了集成鼠标component之后的输出结果。


图12.4 Output of the integrated mouse component.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值