普通桌面应用程序
普通桌面应用程序主要是指Win32常规应用程序框架、MFC常规框架、C#的WPF框架、Java的Swing框架等开发出来的程序。大多采用的是“事件驱动机制”。
如图所示:
其中WinProc()函数表示的是桌面应用程序处理函数,当用户和Windows运行任务时,那么窗口或者其他应用程序窗口会产生事件和消息。所有消息都会进入一个队列中,而每个窗口的消息会发送到每个窗口的专用队列中,然后主事件循环检测这些消息并且将它们发送到相应的窗口处理函数WinProc()中进行处理。
而WinMain()则表示该桌面应用程序的入口地址处。
下图所示为Windows事件循环处理消息机制:
由此并可以看出,普通的桌面应用程序,会通过事件来驱动程序,然后调用相应的事件处理函数,来进行绘制和响应。而对于游戏程序,不能采用“事件驱动机制”,因为游戏是实时响应的,如果采用“事件驱动机制”,那么当玩家所触发的事件不多时,勉强能够应对,但一旦当所需处理的事件过多时,明显就显得有些“鸡肋”。如当玩家与商人进行对话买卖装备时,这时程序将会产生与商人对话事件信息、获取装备信息、装备的贩卖价格信息、玩家信息、还要获取相关UI的信息等等事件,这些都会在消息队列中进行排队,等待处理,就算采用多线程并发执行,“事件驱动机制”也会吃不消,无法将多个事件或其中几个事件一起完成。
而“实时响应”机制便能解决这个问题。
如图所示
游戏执行大体流程
因此在开发游戏中,很少选用WIN32框架、MFC框架等其他开发桌面应用程序所自带的输入控制系统。除非这个游戏对实时性要求不高,倒是可以考虑采用。
按键输入
可以得知,普通桌面应用程序的“事件驱动机制”不能满足大部分游戏实时响应的需求,因此通过“按钮事件”来控制游戏,则此方法行不通。
所以将使用DirectX中所提供的DirectInput接口来实现程序对按键的响应。
DirectInput接口在DX8.0版本之后,对键盘和鼠标外设几乎没有较大的更新。
因此,将使用的接口也是DX8.0版本的。
一般游戏循环图
DirectInput是一个不依赖于硬件的虚拟输入系统,它允许硬件制造商开发应用于统一接口下的传统和非传统的输入设备。只需同DirectInput打交道,然后通过DirectInput再将代码翻译成输入设备能够理解的代码。
DirectInput与其他硬件之间的关系结构图
其中HAL(Hardware Abstraction Layer)为硬件抽象层,HEL(Hardware Emulation Layer)为硬件仿真层。不过一般使用HEL不多,不需要进行过多的仿真模拟,用的比较多的还是HAL。
使用步骤:
-
通过调用DirectInput8Create()创建IDirectInput8接口。
-
查询设备的GUID(Globally Unique Identifier)全局唯一标识符,判断已连接了哪些设备,当玩家进行输入时,判断是哪种设备进行了输入。
由于没有过多设备,只有鼠标和键盘,所以可选择是否需要此步。 -
通过调用CreateDevice()传递一个GUID。
-
设置协作等级,使用API—SetCooperativeLevel(),设置DirectInput是独占前台和后台,还是只占用前台,或是非独占。
-
设置每一种设备的数据格式。
-
设置每一种设备的性能,即设备范围特性。
-
从设备中获取数据。
例如
//Keyboard class
CKeyboard::CKeyboard(LPDIRECTINPUT8 input, HWND hwnd)
{
// Initialize the keyboard.
if (input->CreateDevice(GUID_SysKeyboard, &m_device, NULL) == DI_OK)
{
if (m_device->SetDataFormat(&c_dfDIKeyboard) == DI_OK)
{
if (m_device->SetCooperativeLevel(hwnd,
DISCL_FOREGROUND | DISCL_NONEXCLUSIVE) == DI_OK)
{
m_device->Acquire();
}
}
}
// Clear keys will clear out the array of keys we have.
memset(m_keys, 0, sizeof(m_keys));
}
其中input是IDirectInput8所创建的一个对象。
创建设备函数原型:
HRESULT CreateDevice(
REFGUID rguid, //代表是哪种设备的GUID,如键盘,鼠标,还是游戏手柄等
LPDIRECTINPUTDEVICE * lplpDirectInputDevice,//指向所创建的设备对象
LPUNKNOWN pUnkOuter// IUnknown 接口的相关指针,大部分都设置为0
)
设备GUID可选项
DEFINE_GUID(GUID_SysMouse, 0x6F1D2B60,0xD5A0,0x11CF,0xBF,0xC7,0x44,0x45,0x53,0x54,0x00,0x00);
DEFINE_GUID(GUID_SysKeyboard,0x6F1D2B61,0xD5A0,0x11CF,0xBF,0xC7,0x44,0x45,0x53,0x54,0x00,0x00);
DEFINE_GUID(GUID_Joystick ,0x6F1D2B70,0xD5A0,0x11CF,0xBF,0xC7,0x44,0x45,0x53,0x54,0x00,0x00);
设置数据格式
该函数以描述设备返回的数据格式的机构地址为参数,其值可以是c_dfDIKeyboard、c_dfDIMouse、c_dfDIMouse2、c_dfDIJoystick或c_dfDIJoystick2,这取决于正在创建的设备的类型。
HRESULT SetDataFormat(
LPCDIDATAFORMAT lpdf
)
设置设备的合作等级
HRESULT SetCooperativeLevel(
HWND hwnd,//窗口句柄
DWORD dwLevel//指定设备的合作等级
)
dwLevel值 | 描述 |
---|---|
DSSCL_EXCLUSIVE | 系统独占该设备 |
DSSCL_NONEXCLUSIVE | 非独占访问。即访问该设备不会干涉其他访问相同设备的应用程序 |
DSSCL_BACKGROUND | 应用程序无论是在前台还是在后台都可以使用设备 |
DSSCL_FOREGROUND | 应用程序要求前台访问 |
… | … |
调用Acquire()函数。用于获取输入设备
设计类图
CDeviceInterface类设置为接口类,而CKeyboard,CMouse,CGameController则分别为三种不同设备的类。
采用工厂模式,当需要什么对象时,只需实例化相应的对象。
CDeviceInterface
class CDeviceInterface
{
public:
CDeviceInterface() {}
virtual ~CDeviceInterface() {}
virtual bool UpdateDevice() = 0;
virtual int ButtonUp(size_t key) = 0;
virtual int ButtonDown(size_t key) = 0;
virtual void Shutdown() = 0;
};
CInputInterface为输入系统的接口类,CDirectInputSystem为输入系统类,该类里面包含有CKeyboard类,CMouse类和CGameController类。
采用抽象工厂设计模式,当需要什么设备时,只需进行实例化相应的对象即可。
CInputInterface类
class CInputInterface
{
public:
CInputInterface() {}
virtual ~CInputInterface() {}
virtual bool Initialize() = 0;
virtual bool UpdateDevices() = 0;
virtual int KeyUp(size_t key) = 0;
virtual int KeyDown(size_t key) = 0;
virtual int MouseButtonUp(size_t button) = 0;
virtual int MouseButtonDown(size_t button) = 0;
virtual int ControllerButtonUp(size_t button) = 0;
virtual int ControllerButtonDown(size_t button) = 0;
virtual void Shutdown() = 0;
};