本主题介绍在通用 Windows 平台(UWP)游戏中有效使用输入设备的模式和技术。
通过阅读本主题,你将了解:
- 如何跟踪玩家及其当前正在使用的输入和导航设备
- 如何检测按钮状态变化(按下至释放、释放至按下)
- 如何使用单个测试检测复杂按钮排列
选择输入设备类
有许多不同类型的输入 API 可供你使用,例如 ArcadeStick、FlightStick和 Gamepad。 如何确定要用于游戏的 API?
应选择哪个 API 为游戏提供最合适的输入。 例如,如果要制作 2D 平台游戏,可能只需使用 Gamepad 类,而不用其他类提供的额外功能。 这将限制游戏仅支持游戏板,并提供一致的界面,可在许多不同的游戏板中工作,而无需其他代码。
另一方面,对于复杂的飞行和赛车模拟,你可能希望枚举所有 RawGameController 对象作为基线,以确保它们支持爱好者玩家可能拥有的任何特殊设备,包括单独的踏板或油门这样的设备,这些通常仍由单个玩家使用。
在此处,可以使用输入类的 FromGameController 方法(如 Gamepad.FromGameController),以查看每个设备是否有更精心策划的视图。 例如,如果设备也是 游戏手柄,那么你可能需要调整按钮映射 UI 以反映这一点,并提供一些合理的默认按钮映射选项。 (这与要求玩家手动配置游戏板输入(如果仅使用 RawGameController)相反。
或者,您可以查看 RawGameController 的供应商ID(VID)和产品ID(PID)(分别使用 HardwareVendorId 和 HardwareProductId),为常用设备提供建议的按钮映射,同时通过玩家手动映射保持对未来出现的未知设备的兼容性。
跟踪连接的控制器
虽然每个控制器类型都包含连接的控制器列表(例如 Gamepad.Gamepads),但最好维护自己的控制器列表。 有关详细信息,请参阅 游戏板列表(每个控制器类型在其自己的主题上都有类似的命名部分)。
但是,当玩家拔出控制器或插入新控制器时,会发生什么情况? 需要处理这些事件,并相应地更新列表。 有关详细信息,请参阅 添加和删除游戏板(同样,每个控制器类型在其自己的主题上都有类似的命名部分)。
由于添加和删除的事件是异步引发的,因此处理控制器列表时可能会得到不正确的结果。 因此,只要访问控制器列表,就应该将锁放在它周围,以便一次只能有一个线程访问它。 这可以通过 并发运行时来完成,特别是 critical_section 类(在 ppl.h<>中)。
另一个要考虑的事情是,连接的控制器列表最初将为空,需要一两秒钟才能填充。 因此,如果仅在开始方法中分配当前游戏手柄,它将是 null!
为了纠正这一点,你应该有一种方法来“刷新”主游戏板(在单玩家游戏中;多人游戏将需要更复杂的解决方案)。 然后,您应在控制器添加和控制器移除的事件处理程序中,或在更新方法中调用此方法。
以下方法将返回列表中的第一个游戏手柄(如果列表为空,则返回 nullptr)。 然后,每当你对控制器进行操作时,只需记住检查 nullptr。 您可以选择在没有控制器连接时(例如,暂停游戏)是阻止游戏,还是忽略输入让游戏继续进行。
#include <ppl.h>
using namespace Platform::Collections;
using namespace Windows::Gaming::Input;
using namespace concurrency;
Vector<Gamepad^>^ m_myGamepads = ref new Vector<Gamepad^>();
Gamepad^ GetFirstGamepad()
{
Gamepad^ gamepad = nullptr;
critical_section::scoped_lock{ m_lock };
if (m_myGamepads->Size > 0)
{
gamepad = m_myGamepads->GetAt(0);
}
return gamepad;
}
综上所述,下面是一个如何处理游戏手柄输入的示例:
#include <algorithm>
#include <ppl.h>
using namespace Platform::Collections;
using namespace Windows::Foundation;
using namespace Windows::Gaming::Input;
using namespace concurrency;
static Vector<Gamepad^>^ m_myGamepads = ref new Vector<Gamepad^>();
static Gamepad^ m_gamepad = nullptr;
static critical_section m_lock{};
void Start()
{
// Register for gamepad added and removed events.
Gamepad::GamepadAdded += ref new EventHandler<Gamepad^>(&OnGamepadAdded);
Gamepad::GamepadRemoved += ref new EventHandler<Gamepad^>(&OnGamepadRemoved);
// Add connected gamepads to m_myGamepads.
for (auto gamepad : Gamepad::Gamepads)
{
OnGamepadAdded(nullptr, gamepad);
}
}
void Update()
{
// Update the current gamepad if necessary.
if (m_gamepad == nullptr)
{
auto gamepad = GetFirstGamepad();
if (m_gamepad != gamepad)
{
m_gamepad = gamepad;
}
}
if (m_gamepad != nullptr)
{
// Gather gamepad reading.
}
}
// Get the first gamepad in the list.
Gamepad^ GetFirstGamepad()
{
Gamepad^ gamepad = nullptr;
critical_section::scoped_lock{ m_lock };
if (m_myGamepads->Size > 0)
{
gamepad = m_myGamepads->GetAt(0);
}
return gamepad;
}
void OnGamepadAdded(Platform::Object^ sender, Gamepad^ args)
{
// Check if the just-added gamepad is already in m_myGamepads; if it isn't,
// add it.
critical_section::scoped_lock lock{ m_lock };
auto it = std::find(begin(m_myGamepads), end(m_myGamepads), args);
if (it == end(m_myGamepads))
{
m_myGamepads->Append(args);
}
}
void OnGamepadRemoved(Platform::Object^ sender, Gamepad^ args)
{
// Remove the gamepad that was just disconnected from m_myGamepads.
unsigned int indexRemoved;
critical_section::scoped_lock lock{ m_lock };
if (m_myGamepads->IndexOf(args, &indexRemoved))
{
if (m_gamepad == m_myGamepads->GetAt(indexRemoved))
{
m_gamepad = nullptr;
}
m_myGamepads->RemoveAt(indexRemoved);
}
}
跟踪用户及其设备
所有输入设备都与 用户 相关联,以便其标识可以链接到其游戏、成就、设置更改和其他活动。 用户可以随意登录或注销,并且在上一个用户注销后,通常会有其他用户登录到仍然连接到系统的输入设备。当用户登录或注销时,将触发 IGameController.UserChanged 事件。 可以为此事件注册事件处理程序,以跟踪玩家及其正在使用的设备。
用户身份也是将输入设备与其相应的 UI 导航控制器关联的方式。
出于这些原因,应跟踪玩家输入并与设备类 用户 属性(继承自 IGameController 接口)相关联。
GitHub 上的 UserGamepadPairingUWP 示例应用演示了如何跟踪用户及其使用的设备。
检测按钮状态变化
有时,你想知道何时首次按下或释放按钮;也就是说,当按钮状态从释放转换到按下或从按下到释放时。 若要确定这一点,需要记住以前的设备读取并将当前读数与它进行比较,以查看更改的内容。
以下示例演示了一种用于记住上一个阅读的基本方法:游戏板在此处显示,但街机摇杆、赛车方向盘和其他输入设备类型的原则相同。
Gamepad gamepad;
GamepadReading newReading();
GamepadReading oldReading();
// Called at the start of the game.
void Game::Start()
{
gamepad = Gamepad::Gamepads[0];
}
// Game::Loop represents one iteration of a typical game loop
void Game::Loop()
{
// move previous newReading into oldReading before getting next newReading
oldReading = newReading, newReading = gamepad.GetCurrentReading();
// process device readings using buttonJustPressed/buttonJustReleased (see below)
}
在执行任何其他操作之前,Game::Loop 将现有值 newReading(从上一循环迭代读取的游戏板读取)移到 oldReading,然后使用当前迭代的全新游戏板读取填充 newReading。 这为你提供了检测按钮转换所需的信息。
以下示例演示了检测按钮转换的基本方法:
bool ButtonJustPressed(const GamepadButtons selection)
{
bool newSelectionPressed = (selection == (newReading.Buttons & selection));
bool oldSelectionPressed = (selection == (oldReading.Buttons & selection));
return newSelectionPressed && !oldSelectionPressed;
}
bool ButtonJustReleased(GamepadButtons selection)
{
bool newSelectionReleased =
(GamepadButtons.None == (newReading.Buttons & selection));
bool oldSelectionReleased =
(GamepadButtons.None == (oldReading.Buttons & selection));
return newSelectionReleased && !oldSelectionReleased;
}
这两个函数首先从 newReading 和 oldReading派生按钮选择的布尔状态,然后执行布尔逻辑来确定目标转换是否已发生。 仅当新读取包含目标状态(分别按下或释放)并且 旧读取不包含目标状态时,这些函数才会 返回 true;否则,返回 false。
检测复杂按钮排列
输入设备的每个按钮都提供一个数字读数,指示是按下(向下)还是释放(向上)。 为了提高效率,按钮读取不表示为单个布尔值;它们全部打包到由特定于设备的枚举表示的位域,例如 GamepadButtons。 若要读取特定按钮,使用按位掩码来隔离你感兴趣的值。 设置相应的位时,按下按钮(向下);否则,它被释放(上)。
回顾一下如何确定按下或释放单个按钮;游戏手柄在此处显示,但原理与街机摇杆、赛车方向盘及其他输入设备类型相同。
GamepadReading reading = gamepad.GetCurrentReading();
// Determines whether gamepad button A is pressed.
if (GamepadButtons::A == (reading.Buttons & GamepadButtons::A))
{
// The A button is pressed.
}
// Determines whether gamepad button A is released.
if (GamepadButtons::None == (reading.Buttons & GamepadButtons::A))
{
// The A button is released (not pressed).
}
如你所看到的,确定单个按钮的状态是直截了当的,但有时你可能想要确定是按下还是释放多个按钮,或者是否以特定方式排列了一组按钮,有些是按下的,有些不是。 测试多个按钮比测试单个按钮更为复杂,尤其是可能存在的混合按钮状态。但这些测试有一个简单的公式,同样适用于单个和多个按钮的测试。
以下示例确定是否同时按下游戏板按钮 A 和 B:
if ((GamepadButtons::A | GamepadButtons::B) == (reading.Buttons & (GamepadButtons::A | GamepadButtons::B))
{
// The A and B buttons are both pressed.
}
以下示例确定游戏板按钮 A 和 B 是否均已释放:
if ((GamepadButtons::None == (reading.Buttons & GamepadButtons::A | GamepadButtons::B))
{
// The A and B buttons are both released (not pressed).
}
以下示例确定游戏板按钮 A 是否按下,同时按钮 B 是否松开:
if (GamepadButtons::A == (reading.Buttons & (GamepadButtons::A | GamepadButtons::B))
{
// The A button is pressed and the B button is released (B is not pressed).
}
这五个示例共有的公式是,要测试的按钮排列是由相等运算符左侧的表达式指定的,而要考虑的按钮由右侧的掩码表达式选择。
以下示例通过重写前面的示例来更清楚地演示此公式:
auto buttonArrangement = GamepadButtons::A;
auto buttonSelection = (reading.Buttons & (GamepadButtons::A | GamepadButtons::B));
if (buttonArrangement == buttonSelection)
{
// The A button is pressed and the B button is released (B is not pressed).
}
此公式可用于测试其状态的任何排列中的任意数量的按钮。
获取电池的状态
对于实现 IGameControllerBatteryInfo 接口的任何游戏控制器,可以在控制器实例上调用 TryGetBatteryReport,以获取 BatteryReport 对象,该对象提供有关控制器中的电池的信息。 可以获取电池充电速率(ChargeRateInMilliwatts)、新电池的估计能量容量(DesignCapacityInMilliwattHours),以及当前电池的全充电能量容量(FullChargeCapacityInMilliwattHours)。
对于支持详细电池报告的游戏控制器,你可以查看有关电池的详细信息,具体内容请参见 获取电池信息。 但是,大多数游戏控制器不支持该级别的电池报告,而是使用低成本硬件。 对于这些控制器,需要牢记以下注意事项:
ChargeRateInMilliwatts 和 DesignCapacityInMilliwattHours 将始终 NULL。
您可以通过计算 RemainingCapacityInMilliwattHours / FullChargeCapacityInMilliwattHours来获取电池百分比。 应忽略这些属性的值,并且只处理计算的百分比。
上一项的百分比始终是以下之一:
- 100% (完整)
- 70% (中)
- 40% (低)
- 10% (危急)
如果代码根据剩余电量的百分比执行某些操作(如绘制 UI),请确保它符合上述值。 例如,如果要在控制器的电池电量不足时警告播放器,当控制器的电池达到 10%时,请发出警告。