游戏输入实践Input practices for games

本主题介绍通用 Windows 平台 (UWP) 游戏中有效使用输入设备的模式和方法。This topic describes patterns and techniques for effectively using input devices in Universal Windows Platform (UWP) games.

阅读本主题后,你将了解:By reading this topic, you'll learn:

  • 如何跟踪玩家和玩家当前正在使用的输入和导航设备how to track players and which input and navigation devices they're currently using
  • 如何检测按钮转换(按下到释放,释放到按下)how to detect button transitions (pressed-to-released, released-to-pressed)
  • 如何使用一个测试检测复杂的按钮排列how to detect complex button arrangements with a single test

选择输入设备类Choosing an input device class

可使用许多不同类型的输入 API,如 ArcadeStickFlightStickGamepadThere are many different types of input APIs available to you, such as ArcadeStick, FlightStick, and Gamepad. 如何确定要用于游戏的 API?How do you decide which API to use for your game?

应选择可提供最适合于游戏的输入的 API。You should choose whichever API gives you the most appropriate input for your game. 例如,如果在创建 2D 平台游戏,则可能只需使用 Gamepad 类,不会为可通过其他类提供的额外功能而费心。For example, if you're making a 2D platform game, you can probably just use the Gamepad class and not bother with the extra functionality available via other classes. 这会将游戏限制为仅支持游戏板,并提供可适用于许多不同游戏板而无需额外代码的一致接口。This would restrict the game to supporting gamepads only and provide a consistent interface that will work across many different gamepads with no need for additional code.

另一方面,对于复杂的飞行和赛车模拟,可能要枚举所有 RawGameController 对象作为基线,以确保它们支持发烧友玩家可能拥有的任何专业设备,包括诸如仍由单个玩家所使用的单独踏板或油门等设备。On the other hand, for complex flight and racing simulations, you might want to enumerate all of the RawGameController objects as a baseline to make sure they support any niche device that enthusiast players might have, including devices such as separate pedals or throttle that are still used by a single player.

在其中可以使用输入类的 FromGameController 方法(如 Gamepad.FromGameController)查看每个设备是否具有更精选的视图。From there, you can use an input class's FromGameController method, such as Gamepad.FromGameController, to see if each device has a more curated view. 例如,如果设备也是游戏板,则可能要调整按钮映射 UI 以反映这种情况,并提供某些合理的默认按钮映射以进行选择。For example, if the device is also a Gamepad, then you might want to adjust the button mapping UI to reflect that, and provide some sensible default button mappings to choose from. (这与仅使用 RawGameController 时需要玩家手动配置游戏板的情况相反。)(This is in contrast to requiring the player to manually configure the gamepad inputs if you're only using RawGameController.)

或者,可以查看 RawGameController 的供应商 ID (VID) 和产品 ID (PID)(分别使用 HardwareVendorIdHardwareProductId),并为常见设备提供建议的按钮映射,同时仍通过玩家的手动映射来保持与将来推出的未知设备兼容。Alternatively, you can look at the vendor ID (VID) and product ID (PID) of a RawGameController (using HardwareVendorId and HardwareProductId, respectively) and provide suggested button mappings for popular devices while still remaining compatible with unknown devices that come out in the future via manual mappings by the player.

跟踪连接的控制器Keeping track of connected controllers

虽然每个控制器类型均包含连接控制器的列表(如 Gamepad.Gamepads),但最好维护你自己的控制器列表。While each controller type includes a list of connected controllers (such as Gamepad.Gamepads), it is a good idea to maintain your own list of controllers. 请参阅游戏板列表了解详细信息(每个控制器类型均有一个有关其自己主题的名称类似的部分)。See The gamepads list for more information (each controller type has a similarly named section on its own topic).

但是,在玩家拔掉控制器或插入新控制器时会发生什么?However, what happens when the player unplugs their controller, or plugs in a new one? 你需要处理这些事件,并相应更新列表。You need to handle these events, and update your list accordingly. 请参阅添加和删除游戏板了解详细信息(同样,每个控制器类型均有一个有关其自己主题的名称类似的部分)。See Adding and removing gamepads for more information (again, each controller type has a similarly named section on its own topic).

由于添加和删除的事件异步引发,因此在处理控制器列表时可能会收到错误结果。Because the added and removed events are raised asynchronously, you could get incorrect results when dealing with your list of controllers. 因此,不论何时访问控制器列表,都应在列表周围放置锁定,以便一次只有一个线程可以访问它。Therefore, anytime you access your list of controllers, you should put a lock around it so that only one thread can access it at a time. 这可以通过并发运行时实现,特别是 <ppl.h> 中的 critical_section 类This can be done with the Concurrency Runtime, specifically the critical_section class, in <ppl.h>.

另一个需要考虑的事项是连接的控制器列表最初是空的,需要一到两秒时间填充。Another thing to think about is that the list of connected controllers will initially be empty, and takes a second or two to populate. 因此,如果你仅在 Start 方法中分配当前游戏板,它将为 nullSo if you only assign the current gamepad in the start method, it will be null!

若要解决此问题,你应该有“刷新”主游戏板的方法(单人游戏中;多人游戏需要更复杂的解决方案)。To rectify this, you should have a method that "refreshes" the main gamepad (in a single-player game; multiplayer games will require more sophisticated solutions). 然后,应在添加控制器和删除控制器事件处理程序中,或使用更新方法来调用此方法。You should then call this method in both your controller added and controller removed event handlers, or in your update method.

以下方法只返回列表中的第一个游戏板(或者如果列表为空返回 nullptr)。The following method simply returns the first gamepad in the list (or nullptr if the list is empty). 然后,当你在任何时间执行任何操作时,只需记住检查 nullptr 即可。Then you just need to remember to check for nullptr anytime you do anything with the controller. 它由你在未连接控制器时是要阻止游戏(例如,通过暂停游戏)还是让游戏继续来决定,同时将忽略输入。It's up to you whether you want to block gameplay when there's no controller connected (for example, by pausing the game) or simply have gameplay continue, while ignoring input.

#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;
}

组织到一起,下面是如何处理通过游戏板输入的示例:Putting it all together, here is an example of how to handle input from a 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);
    }
}

跟踪用户及其设备Tracking users and their devices

所有输入设备都与一位用户关联,以便可以将其身份与其游戏玩法、成就、设置更改和其他活动关联。All input devices are associated with a User so that their identity can be linked to their gameplay, achievements, settings changes, and other activities. 用户可以随意登录或注销,而且,其他用户在之前的用户注销后登录到与系统保持相连的设备也很常见。用户登录或注销时会引发 IGameController.UserChanged 事件。Users can sign in or sign out at will, and it's common for a different user to sign in on an input device that remains connected to the system after the previous user has signed out. When a user signs in or out, the IGameController.UserChanged event is raised. 你可以为此事件注册事件处理程序,以跟踪玩家和他们正在使用的设备。You can register an event handler for this event to keep track of players and the devices they're using.

用户标识也是输入设备与其相应的 UI 导航控制器关联的方式。User identity is also the way that an input device is associated with its corresponding UI navigation controller.

因此,应该使用设备类(继承自 IGameController 接口)的 User 属性跟踪和关联玩家。For these reasons, player input should be tracked and correlated with the User property of the device class (inherited from the IGameController interface).

UserGamepadPairingUWP示例演示了如何跟踪用户及其正在使用的设备。The UserGamepadPairingUWP sample demonstrates how you can keep track of users and the devices they're using.

检测按钮转换Detecting button transitions

有时你可能需要了解何时第一次按下或释放按钮;即,按钮状态从释放转换为按下或从按下转化为释放的准确时间。Sometimes you want to know when a button is first pressed or released; that is, precisely when the button state transitions from released to pressed or from pressed to released. 要确定准确时间,你需要记住之前的设备读数并将当前的读数与之比较,以查看发生了哪些变化。To determine this, you need to remember the previous device reading and compare the current reading against it to see what's changed.

以下示例演示了记住之前读数的基本方法;游戏板如下所示,但街机摇杆、赛车方向盘和其他输入设备类型的原则相同。The following example demonstrates a basic approach for remembering the previous reading; gamepads are shown here, but the principles are the same for arcade stick, racing wheel, and the other input device types.

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 中,然后用当前迭代的全新游戏板读数填充 newReadingBefore doing anything else, Game::Loop moves the existing value of newReading (the gamepad reading from the previous loop iteration) into oldReading, then fills newReading with a fresh gamepad reading for the current iteration. 可以为你提供检测按钮转换所需的信息。This gives you the information you need to detect button transitions.

以下示例演示了检测按钮转换的基本方法:The following example demonstrates a basic approach for detecting button transitions:

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;
}

这两个功能先从 newReadingoldReading 中派生按钮选择的布尔状态,然后执行布尔逻辑以确定是否已出现目标转换。These two functions first derive the Boolean state of the button selection from newReading and oldReading, then perform Boolean logic to determine whether the target transition has occurred. 仅当新读数包含目标状态(分别为按下或释放)旧读数不包含目标状态时,这些功能返回 true,否则返回 falseThese functions return true only if the new reading contains the target state (pressed or released, respectively) and the old reading does not also contain the target state; otherwise, they return false.

检测复杂的按钮排列Detecting complex button arrangements

每个输入设备按钮均提供指示是按下(向下)还是释放(向上)的数字读数。Each button of an input device provides a digital reading that indicates whether it's pressed (down) or released (up). 为了提高效率,按钮读数不以单独的布尔值表示;而是全部打包到一个由特定于设备的枚举(例如 GamepadButtons)表示的位域中。For efficiency, button readings aren't represented as individual boolean values; instead, they're all packed into bitfields represented by device-specific enumerations such as GamepadButtons. 若要读取特定按钮,使用按位掩码隔离你感兴趣的值。To read specific buttons, bitwise masking is used to isolate the values that you're interested in. 设置相应位时按钮为按下(向下);否则,按钮为释放(向上)。A button is pressed (down) when its corresponding bit is set; otherwise, it's released (up).

回想确定按下或释放按钮的方式;游戏板如下所示,但街机摇杆、赛车方向盘和其他输入设备类型的原则相同。Recall how single buttons are determined to be pressed or released; gamepads are shown here, but the principles are the same for arcade stick, racing wheel, and the other input device types.

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).
}

如你所见,确定单个按钮的状态很简单,但有时你可能需要确定:是按下还是释放多个按钮,或者是否按特定方式安排一组按钮(按下一些按钮,释放一些按钮)。As you can see, determining the state of a single button is straight-forward, but sometimes you might want to determine whether multiple buttons are pressed or released, or if a set of buttons are arranged in a particular way—some pressed, some not. 测试多个按钮比测试单个按钮更复杂,特别是可能存在混合按钮状态,但是对于这些适用于单个和多个相似的按钮测试有一个简单的公式。Testing multiple buttons is more complex than testing single buttons—especially with the potential of mixed button state—but there's a simple formula for these tests that applies to single and multiple button tests alike.

以下示例确定是否同时按下游戏板按钮 A 和 B:The following example determines whether gamepad buttons A and B are both pressed:

if ((GamepadButtons::A | GamepadButtons::B) == (reading.Buttons & (GamepadButtons::A | GamepadButtons::B))
{
    // The A and B buttons are both pressed.
}

以下示例确定是否同时释放游戏板按钮 A 和 B:The following example determines whether gamepad buttons A and B are both released:

if ((GamepadButtons::None == (reading.Buttons & GamepadButtons::A | GamepadButtons::B))
{
    // The A and B buttons are both released (not pressed).
}

以下示例确定是否在释放按钮 B 时按下按钮 A:The following example determines whether gamepad button A is pressed while button B is released:

if (GamepadButtons::A == (reading.Buttons & (GamepadButtons::A | GamepadButtons::B))
{
    // The A button is pressed and the B button is released (B is not pressed).
}

这五个示例共同拥有的公式是:由相等运算符左侧的表达式指定要测试的按钮排列,而由右侧的掩码表达式选择要考虑的按钮。The formula that all five of these examples have in common is that the arrangement of buttons to be tested for is specified by the expression on the left-hand side of the equality operator while the buttons to be considered are selected by the masking expression on the right-hand side.

通过重新编写之前的示例,以下示例更清楚地演示了此公式:The following example demonstrates this formula more clearly by rewriting the previous example:

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).
}

此公式可应用于在任意状态排列中测试任何按钮数。This formula can be applied to test any number of buttons in any arrangement of their states.

获取电池状态Get the state of the battery

对于实现 IGameControllerBatteryInfo 界面的任何游戏控制器,你可以调用控制器实例上的 TryGetBatteryReport 来获取提供控制器中电池信息的 BatteryReport 对象。For any game controller that implements the IGameControllerBatteryInfo interface, you can call TryGetBatteryReport on the controller instance to get a BatteryReport object that provides information about the battery in the controller. 你可以获取诸如电池正在充电的速度 (ChargeRateInMilliwatts)、新电池的估计能量容量 (DesignCapacityInMilliwattHours) 以及当前电池的完整充电电量 (FullChargeCapacityInMilliwattHours) 这样的属性。You can get properties like the rate that the battery is charging (ChargeRateInMilliwatts), the estimated energy capacity of a new battery (DesignCapacityInMilliwattHours), and the fully-charged energy capacity of the current battery (FullChargeCapacityInMilliwattHours).

对于支持详细电池报告的游戏控制器,你可以获取有关电池的这一信息及更多信息,在获取电池信息中作了详细介绍。For game controllers that support detailed battery reporting, you can get this and more information about the battery, as detailed in Get battery information. 但是,大部分游戏控制器不支持该电池报告级别,而是使用成本较低的硬件。However, most game controllers don't support that level of battery reporting, and instead use low-cost hardware. 对于这些控制器,你需要记住以下注意事项:For these controllers, you'll need to keep the following considerations in mind:

  • ChargeRateInMilliwattsDesignCapacityInMilliwattHours 将始终为 NULLChargeRateInMilliwatts and DesignCapacityInMilliwattHours will always be NULL.

  • 可以通过计算RemainingCapacityInMilliwattHours / FullChargeCapacityInMilliwattHours获取电池百分比。You can get the battery percentage by calculating RemainingCapacityInMilliwattHours / FullChargeCapacityInMilliwattHours. 你应该忽略这些属性的值,而仅处理计算得出的百分比。You should ignore the values of these properties and only deal with the calculated percentage.

  • 上一条中所述的百分比始终是以下值之一:The percentage from the previous bullet point will always be one of the following:

    • 100%(满)100% (Full)
    • 70%(中)70% (Medium)
    • 40%(低)40% (Low)
    • 10%(临界)10% (Critical)

如果你的代码根据剩余的电池使用时间百分比执行某些操作(如绘制 UI),请确保它与上面的值相符。If your code performs some action (like drawing UI) based on the percentage of battery life remaining, make sure that it conforms to the values above. 例如,如果你想要警告玩家控制器的电池电量不足,则在到达 10% 时执行此操作。For example, if you want to warn the player when the controller's battery is low, do so when it reaches 10%.

另请参阅See also