游戏手柄和振动

此页介绍使用 Windows.Gaming.Input.Gamepad[游戏手柄] 和通用 Windows 平台 (UWP) 的相关 API 进行游戏手柄编程的基本知识。

在本页中,你将了解如下内容:

  • 如何收集已连接的游戏手柄及其用户的列表
  • 如何检测已添加或已移除的游戏手柄
  • 如何从一个或多个游戏手柄读取输入
  • 如何发送振动和脉冲命令
  • 游戏手柄如何具有和 UI 导航设备一样的行为

游戏手柄概述

Xbox 无线控制器和 Xbox 无线控制器 S 等游戏手柄是常规用途游戏输入设备。 它们是 Xbox One 上的标准输入设备,也是 Windows 游戏玩家在不喜欢键盘和鼠标时的常见选择。 Windows 10 或 Windows 11 和 Xbox UWP 应用通过 Windows.Gaming.Input 命名空间支持游戏手柄。

Xbox One 游戏板配备有方向键(或称为 D-pad);A、B、X、Y、视图和菜单按钮;左右操纵杆、缓冲键和扳机键;以及总共 4 个振动电机。 两个控制杆可沿 X 轴和 Y 轴提供双重模拟读数,并在向内按下时充当按钮。 每个触发器会提供一个模拟读数,表示它被拉回的距离。

注意

Windows.Gaming.Input.Gamepad 还支持 Xbox 360 游戏板,与标准 Xbox One 游戏板具有相同的控件布局。

振动和脉冲扳机键

Xbox One 游戏手柄提供了两个独立马达,可产生强烈和细微的游戏手柄震动,另外提供两个专用马达,可向每个扳机键提供剧烈震动(这一独特功能是将 Xbox One 游戏手柄扳机键称为脉冲扳机键的原因)。

注意

Xbox 360 游戏板未配备“脉动扳机键”

有关详细信息,请参阅《振动和脉冲扳机键概述》。

控制杆死区

理想状态下,位于中心位置的静态控制杆每次会在 X 轴和 Y 轴上生成相同的中性读数。 但是,由于机械力和控制杆的敏感性,中心位置的实际读数仅近似于理想的中性值,并且后续读数之间可能发生变化。 因此,你必须始终使用小“盲区”(被忽略的靠近理想中心位置的值的范围)来弥补制造差异、机械磨损或者其他游戏板问题

较大的死区为区分有意输入和无意输入提供了一个简单的策略。

有关详细信息,请参阅《读取控制杆》。

UI 导航

为了减轻支持不同输入设备进行用户界面导航造成的负担,并促进游戏和设备之间的一致性,大多数物理输入设备会同时充当独立的逻辑输入设备(称为 UI 导航控制器)。 UI 导航控制器可跨各种输入设备提供通用的 UI 导航命令词汇。

游戏板作为 UI 导航控制器,将导航命令的必需组映射为左操纵杆、方向键、视图、菜单、A 和 B 按钮

导航命令 游戏手柄输入
向上 左控制杆向上/方向键向上
向下 左控制杆向下/方向键向下
Left 左控制杆向左/方向键向左
Right 左控制杆向右/方向键向右
视图 视图按钮
菜单 “菜单”按钮
Accept A 按钮
Cancel B 按钮

此外,游戏手柄会将导航命令的所有可选集映射到剩余的输入。

导航命令 游戏手柄输入
Page Up 左扳机键
Page Down 右扳机键
向左翻页键 左缓冲键
向右翻页键 右缓冲键
向上滚动 右操纵杆向上
向下滚动 右操纵杆向下
向左滚动键 右操纵杆向左
向右滚动键 右操纵杆向右
上下文 1 X 按钮
上下文 2 Y 按钮
上下文 3 左控制杆按键
上下文 4 右控制杆按键

检测和跟踪游戏手柄

游戏手柄由系统管理,因此无需创建或初始化它们。 系统提供连接的游戏手柄和事件列表,以在添加或移除游戏手柄时通知你。

游戏手柄列表

Gamepad 类提供静态属性 Gamepads,它是当前连接的游戏手柄的只读列表。 由于你可能只对某些连接的游戏板感兴趣,所以建议你保留自己的集合,而不必通过 Gamepads 属性来进行访问。

以下示例将所有连接的游戏手柄复制到新集合中。 请注意,由于后台中的其他线程将访问此集合(在 GamepadAddedGamepadRemoved 事件中),你需要在读取或更新集合的任何代码周围进行锁定。

auto myGamepads = ref new Vector<Gamepad^>();
critical_section myLock{};

for (auto gamepad : Gamepad::Gamepads)
{
    // Check if the gamepad is already in myGamepads; if it isn't, add it.
    critical_section::scoped_lock lock{ myLock };
    auto it = std::find(begin(myGamepads), end(myGamepads), gamepad);

    if (it == end(myGamepads))
    {
        // This code assumes that you're interested in all gamepads.
        myGamepads->Append(gamepad);
    }
}
private readonly object myLock = new object();
private List<Gamepad> myGamepads = new List<Gamepad>();
private Gamepad mainGamepad;

private void GetGamepads()
{
    lock (myLock)
    {
        foreach (var gamepad in Gamepad.Gamepads)
        {
            // Check if the gamepad is already in myGamepads; if it isn't, add it.
            bool gamepadInList = myGamepads.Contains(gamepad);

            if (!gamepadInList)
            {
                // This code assumes that you're interested in all gamepads.
                myGamepads.Add(gamepad);
            }
        }
    }   
}

添加和移除游戏手柄

添加或删除游戏板时,会触发 GamepadAddedGamepadRemoved 事件。 可以注册这些事件的处理程序,以跟踪当前连接的游戏手柄。

下面是开始跟踪已添加的游戏手柄的示例。

Gamepad::GamepadAdded += ref new EventHandler<Gamepad^>(Platform::Object^, Gamepad^ args)
{
    // Check if the just-added gamepad is already in myGamepads; if it isn't, add
    // it.
    critical_section::scoped_lock lock{ myLock };
    auto it = std::find(begin(myGamepads), end(myGamepads), args);

    if (it == end(myGamepads))
    {
        // This code assumes that you're interested in all new gamepads.
        myGamepads->Append(args);
    }
}
Gamepad.GamepadAdded += (object sender, Gamepad e) =>
{
    // Check if the just-added gamepad is already in myGamepads; if it isn't, add
    // it.
    lock (myLock)
    {
        bool gamepadInList = myGamepads.Contains(e);

        if (!gamepadInList)
        {
            myGamepads.Add(e);
        }
    }
};

下面是停止跟踪已删除的游戏板的示例。 你还需要处理所跟踪的游戏板被删除时要发生的操作;例如,此代码只跟踪一个游戏板的输入,并在板删除时仅将其设置为 nullptr。 如果游戏板处于活动状态,则需要检查每个帧,并在控制器连接或断开时更新从中收集输入信息的游戏板。

Gamepad::GamepadRemoved += ref new EventHandler<Gamepad^>(Platform::Object^, Gamepad^ args)
{
    unsigned int indexRemoved;
    critical_section::scoped_lock lock{ myLock };

    if(myGamepads->IndexOf(args, &indexRemoved))
    {
        if (m_gamepad == myGamepads->GetAt(indexRemoved))
        {
            m_gamepad = nullptr;
        }

        myGamepads->RemoveAt(indexRemoved);
    }
}
Gamepad.GamepadRemoved += (object sender, Gamepad e) =>
{
    lock (myLock)
    {
        int indexRemoved = myGamepads.IndexOf(e);

        if (indexRemoved > -1)
        {
            if (mainGamepad == myGamepads[indexRemoved])
            {
                mainGamepad = null;
            }

            myGamepads.RemoveAt(indexRemoved);
        }
    }
};

有关详细信息,请参阅游戏输入实践

用户和耳机

每个游戏手柄都可以与用户帐户关联,以将其标识链接到其游戏,并且可以连接耳机,以方便语音聊天或游戏内功能。 要了解有关使用用户和耳机的详细信息,请参阅《跟踪用户及其设备》和《耳机》。

读取游戏手柄

确定感兴趣的游戏手柄后,即可从中收集输入。 但与你可能习惯的一些其他类型的输入不同,游戏手柄不会通过引发事件来传达状态更改。 相反,你需要通过对它们进行“轮询”来定期读取其当前状态。

轮询游戏手柄

轮询可在精确时间点捕获导航设备的快照。 这种收集输入的方法非常适合大多数游戏,因为它们的逻辑通常运行在一个确定的循环中,而不是由事件驱动;通常情况下,根据一次收集的输入解释游戏命令也会比据一段时间内收集的许多单个输入进行解释更加简单。

可通过调用 GetCurrentReading 来轮询游戏手柄;此函数会返回包含游戏手柄状态的 GamepadReading

以下示例轮询游戏手柄的当前状态。

auto gamepad = myGamepads[0];

GamepadReading reading = gamepad->GetCurrentReading();
Gamepad gamepad = myGamepads[0];

GamepadReading reading = gamepad.GetCurrentReading();

除了游戏手柄状态之外,每个读数还包括一个精确指示检索状态时间的时间戳。 该时间戳对于关联之前读数的时间或者游戏模拟的时间非常有用。

读取控制杆

每个控制杆可在 X 轴和 Y 轴上提供介于 -1.0 和 +1.0 之间的模拟读数。 在 X 轴上,值为 -1.0 对应控制杆的最左侧位置;值 +1.0 对应最右侧位置。 在 Y 轴上,值为 -1.0 对应控制杆的最底部位置;值 +1.0 对应最顶部的位置。 在这两个轴中,当摇杆位于中心位置时,值接近于 0.0,但即使在后续读数之间,精确值通常也会有所差异;本节稍后会讨论减小此差异的策略。

左控制杆 X 轴的值是通过 GamepadReading 结构的 LeftThumbstickX 属性读取的,Y 轴的值是通过 LeftThumbstickY 属性来读取的。 右控制杆 X 轴的值是从 RightThumbstickX 属性读取的;Y 轴的值是从 RightThumbstickY 属性读取的。

float leftStickX = reading.LeftThumbstickX;   // returns a value between -1.0 and +1.0
float leftStickY = reading.LeftThumbstickY;   // returns a value between -1.0 and +1.0
float rightStickX = reading.RightThumbstickX; // returns a value between -1.0 and +1.0
float rightStickY = reading.RightThumbstickY; // returns a value between -1.0 and +1.0
double leftStickX = reading.LeftThumbstickX;   // returns a value between -1.0 and +1.0
double leftStickY = reading.LeftThumbstickY;   // returns a value between -1.0 and +1.0
double rightStickX = reading.RightThumbstickX; // returns a value between -1.0 and +1.0
double rightStickY = reading.RightThumbstickY; // returns a value between -1.0 and +1.0

读取控制杆值时,你会注意到,当控制杆处于中心位置时,它们不会可靠地生成 0.0 的中性读数:相反,每次移动控制杆并返回到中心位置时,它们都会生成接近 0.0 的不同值。 要减小这些误差,你可以使用小“死区”(一系列被忽略的接近理想中心位置的值)。 实现死区的一种方法是确定控制杆离开中心位置的距离,并忽略比你选择的距离更近的读数。 可以使用勾股定理粗略计算这一距离,但并不精确,因为操纵杆读数本质上是极值,不是平面值。 这会生成一个径向死区。

以下示例使用勾股定理演示了基本的径向死区。

float leftStickX = reading.LeftThumbstickX;   // returns a value between -1.0 and +1.0
float leftStickY = reading.LeftThumbstickY;   // returns a value between -1.0 and +1.0

// choose a deadzone -- readings inside this radius are ignored.
const float deadzoneRadius = 0.1;
const float deadzoneSquared = deadzoneRadius * deadzoneRadius;

// Pythagorean theorem -- for a right triangle, hypotenuse^2 = (opposite side)^2 + (adjacent side)^2
auto oppositeSquared = leftStickY * leftStickY;
auto adjacentSquared = leftStickX * leftStickX;

// accept and process input if true; otherwise, reject and ignore it.
if ((oppositeSquared + adjacentSquared) > deadzoneSquared)
{
    // input accepted, process it
}
double leftStickX = reading.LeftThumbstickX;   // returns a value between -1.0 and +1.0
double leftStickY = reading.LeftThumbstickY;   // returns a value between -1.0 and +1.0

// choose a deadzone -- readings inside this radius are ignored.
const double deadzoneRadius = 0.1;
const double deadzoneSquared = deadzoneRadius * deadzoneRadius;

// Pythagorean theorem -- for a right triangle, hypotenuse^2 = (opposite side)^2 + (adjacent side)^2
double oppositeSquared = leftStickY * leftStickY;
double adjacentSquared = leftStickX * leftStickX;

// accept and process input if true; otherwise, reject and ignore it.
if ((oppositeSquared + adjacentSquared) > deadzoneSquared)
{
    // input accepted, process it
}

每个控制杆在向内按下时也会充当按钮;有关读取此输入的详细信息,请参阅《读取按钮》。

读取扳机键

扳机键表示为 0.0(完全释放)和 1.0 之间的浮点值(完全按下)。 左扳机键的值是通过 GamepadReading 结构的 LeftTrigger 属性读取的,右扳机键值是通过 RightTrigger 属性来读取的。

float leftTrigger  = reading.LeftTrigger;  // returns a value between 0.0 and 1.0
float rightTrigger = reading.RightTrigger; // returns a value between 0.0 and 1.0
double leftTrigger = reading.LeftTrigger;  // returns a value between 0.0 and 1.0
double rightTrigger = reading.RightTrigger; // returns a value between 0.0 and 1.0

读取按钮

每个游戏板按钮(四个方向的方向键、左右缓冲键、左右操纵杆按键、A、B、X、Y、视图和菜单)均有一个数字读数,指示是按下(向下)还是释放(向上)。 为提高效率,按钮读数不以单独的布尔值表示,而是全部打包到一个由 GamepadButtons 枚举表示的单独位域中。

按钮值是通过 GamepadReading 结构的 Buttons 属性读取的。 因为此属性是位域,所以使用按位掩码隔离你感兴趣的按钮值。 设置相应位时按钮为按下(向下);否则,按钮为释放(向上)。

以下示例确定是否按下了 A 按钮。

if (GamepadButtons::A == (reading.Buttons & GamepadButtons::A))
{
    // button A is pressed
}
if (GamepadButtons.A == (reading.Buttons & GamepadButtons.A))
{
    // button A is pressed
}

以下示例确定是否释放了 A 按钮。

if (GamepadButtons::None == (reading.Buttons & GamepadButtons::A))
{
    // button A is released
}
if (GamepadButtons.None == (reading.Buttons & GamepadButtons.A))
{
    // button A is released
}

有时你可能需要确定:何时将按钮从按下转换为释放或从释放转换为按下,是按下多个按钮还是释放多个按钮,或者是否按特定方式安排一组按钮(分别按下和释放一些按钮)。 有关如何检测这些条件的详细信息,请参阅检测按钮转换检测复杂按钮安排

运行游戏手柄输入示例

GamepadUWP 示例 (github) 演示如何连接到游戏手柄并读取其状态。

振动和脉冲扳机键概述

游戏手柄内的振动马达用于向用户提供触觉反馈。 游戏可利用此功能创造更加深刻的沉浸感、帮助传达状态信息(如受到伤害)、发出接近重要物体的信号或其他创造性用途。

Xbox One 游戏手柄总共配备了四个独立的振动马达。 其中两个大型电机位于游戏板中,左电机提供强烈的高幅振动,右电机提供较轻柔细微的振动。 另外提供了两个小型马达,每个扳机键内配置一个,用于直接向用户操作扳机键的手指提供剧烈突发振动;Xbox One 游戏手柄的这种独特功能是其扳机键被称为脉冲扳机键的原因。 通过协调组合这些马达,可以产生各种触觉。

使用振动和脉冲

游戏手柄振动是通过 Gamepad 类的 Vibration 属性来控制的。 VibrationGamepadVibration 结构的实例,该结构由四个浮点值组成,每个值代表其中一个电机的强度。

尽管可以直接修改 Gamepad.Vibration 属性的成员,但是建议将单独的 GamepadVibration 实例初始化为你需要的值,然后将该值复制到 Gamepad.Vibration 属性来一次性更改实际电机强度。

以下示例演示如何一次性更改马达强度。

// get the first gamepad
Gamepad^ gamepad = Gamepad::Gamepads->GetAt(0);

// create an instance of GamepadVibration
GamepadVibration vibration;

// ... set vibration levels on vibration struct here

// copy the GamepadVibration struct to the gamepad
gamepad.Vibration = vibration;
// get the first gamepad
Gamepad gamepad = Gamepad.Gamepads[0];

// create an instance of GamepadVibration
GamepadVibration vibration = new GamepadVibration();

// ... set vibration levels on vibration struct here

// copy the GamepadVibration struct to the gamepad
gamepad.Vibration = vibration;

使用振动马达

左侧和右侧振动马达采用介于 0.0(无振动)和 1.0 之间(最强烈振动)的浮点值。 左侧马达的强度可通过 GamepadVibration 结构的 LeftMotor 属性设置,右侧马达的强度通过 RightMotor 属性来设置。

以下示例设置振动马达的强度和激活游戏手柄振动。

GamepadVibration vibration;
vibration.LeftMotor = 0.80;  // sets the intensity of the left motor to 80%
vibration.RightMotor = 0.25; // sets the intensity of the right motor to 25%
gamepad.Vibration = vibration;
GamepadVibration vibration = new GamepadVibration();
vibration.LeftMotor = 0.80;  // sets the intensity of the left motor to 80%
vibration.RightMotor = 0.25; // sets the intensity of the right motor to 25%
mainGamepad.Vibration = vibration;

请记住,由于两个马达不同,因此将这些属性设置为相同的值不会在一个马达中产生与另一个马达相同的振动。 对于任意相同的值,左电机会比右电机以更低的频率生成更强烈的振动,右电机会以更高的频率生成更轻柔的振动。 即使在最大值下,左侧马达也不能产生右侧马达的高频率,右侧马达也不能产生左侧马达的强力度。 尽管如此,因为马达由游戏手柄主体刚性连接,所以即使马达具有不同的特性并且可以按不同的强度振动,玩家也不能完全独立地体验振动。 与相同的马达相比,这种布置允许产生更宽广、更有表现力的感觉范围。

使用脉冲扳机键

每个脉冲扳机键马达采用介于 0.0(无振动)和 1.0(最强烈的振动)之间的浮点值。 左扳机键马达的强度可通过 GamepadVibration 结构的 LeftTrigger 属性设置,右扳机键的强度通过 RightTrigger 属性来设置。

以下示例设置两个脉冲扳机键的强度并激活它们。

GamepadVibration vibration;
vibration.LeftTrigger = 0.75;  // sets the intensity of the left trigger to 75%
vibration.RightTrigger = 0.50; // sets the intensity of the right trigger to 50%
gamepad.Vibration = vibration;
GamepadVibration vibration = new GamepadVibration();
vibration.LeftTrigger = 0.75;  // sets the intensity of the left trigger to 75%
vibration.RightTrigger = 0.50; // sets the intensity of the right trigger to 50%
mainGamepad.Vibration = vibration;

与其他马达不同,扳机键内的两个振动马达是相同的,因此它们对于相同的值会在同一个马达中产生相同的振动。 但是,由于这些马达不是通过任何方式刚性连接的,因此玩家会独立体验振动。 这种安排允许完全独立的感觉同时定向到两个触发器,并帮助它们传递比游戏手柄主体中的马达更具体的信息。

运行游戏手柄振动示例

GamepadVibrationUWP 示例 (github) 演示了游戏手柄振动马达和脉冲扳机键如何用于产生各种效果。

另请参阅