65.9K
CodeProject 正在变化。 阅读更多。
Home

使用 XInput 在托管代码中访问 Xbox 360 控制器

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.58/5 (9投票s)

2012年11月11日

CPOL

8分钟阅读

viewsIcon

83921

downloadIcon

6044

使用 XInput 库与 Xbox 360 控制器进行交互。

引言

几年前,我曾写过一篇关于如何在 Windows 窗体应用程序中使用 XNA 访问 Xbox 360 控制器的文章。有人问我如何在不适合使用 XNA 程序集的环境中访问控制器。因此,我编写了这段代码,作为在不使用 XNA 程序集的情况下访问控制器的示例。这仍然是一个托管程序。但它利用 P/Invoke 来使用 DirectX 库提供的功能。

必备组件

我使用 Visual Studio 2012 编写了这段代码。它使用了 XInput dll,而该 dll 已是 Windows 的一部分。因此,无需额外安装即可使用此代码。

什么是 XInput

DirectX 由许多库组成。有用于渲染 3D 图形的 Direct3D,用于播放声音的 DirectSound 等等。DirectInput 包含访问各种输入设备(操纵杆、键盘等)的功能。但 Xbox 360 控制器并非通过该库访问。它通过一个名为 XInput 的不同库访问。该库专门用于 Xbox 360 控制器。让我们看看 C 语言头文件中有哪些内容,以了解它提供了哪些功能。在我的电脑上,我可以在 C:\Program Files (x86)\Windows Kits\8.0\Include\um\Xinput.h 找到头文件。

头文件以定义我们将找到功能的 DLL 名称开始。

#if(_WIN32_WINNT >= _WIN32_WINNT_WIN8)
#define XINPUT_DLL_A  "xinput1_4.dll"
#define XINPUT_DLL_W L"xinput1_4.dll"
#else 
#define XINPUT_DLL_A  "xinput9_1_0.dll"
#define XINPUT_DLL_W L"xinput9_1_0.dll"
#endif

当程序针对 Windows 8 之前的操作系统版本进行编译时,功能位于 xinput1_4.dll 中。对于为 Windows 8 编译的程序,将使用 xinput9_1_0.dll 的功能。如果您想同时支持这两个平台,可以使用旧版 DLL (xinput9_1_0.dll)。它在 Windows 8 上也能正常工作。但较新版本的 DLL (xinput1_4.dll) 包含访问电池的功能。

//
// Structures used by XInput APIs
//
typedef struct _XINPUT_GAMEPAD
{
    WORD                                wButtons;
    BYTE                                bLeftTrigger;
    BYTE                                bRightTrigger;
    SHORT                               sThumbLX;
    SHORT                               sThumbLY;
    SHORT                               sThumbRX;
    SHORT                               sThumbRY;
} XINPUT_GAMEPAD, *PXINPUT_GAMEPAD;

typedef struct _XINPUT_STATE
{
    DWORD                               dwPacketNumber;
    XINPUT_GAMEPAD                      Gamepad;
} XINPUT_STATE, *PXINPUT_STATE;

typedef struct _XINPUT_VIBRATION
{
    WORD                                wLeftMotorSpeed;
    WORD                                wRightMotorSpeed;
} XINPUT_VIBRATION, *PXINPUT_VIBRATION;

typedef struct _XINPUT_CAPABILITIES
{
    BYTE                                Type;
    BYTE                                SubType;
    WORD                                Flags;
    XINPUT_GAMEPAD                      Gamepad;
    XINPUT_VIBRATION                    Vibration;
} XINPUT_CAPABILITIES, *PXINPUT_CAPABILITIES;

还有一个部分包含随 Windows 8 一起推出的新功能结构。

#if(_WIN32_WINNT >= _WIN32_WINNT_WIN8)

typedef struct _XINPUT_BATTERY_INFORMATION
{
    BYTE BatteryType;
    BYTE BatteryLevel;
} XINPUT_BATTERY_INFORMATION, *PXINPUT_BATTERY_INFORMATION;

typedef struct _XINPUT_KEYSTROKE
{
    WORD    VirtualKey;
    WCHAR   Unicode;
    WORD    Flags;
    BYTE    UserIndex;
    BYTE    HidCode;
} XINPUT_KEYSTROKE, *PXINPUT_KEYSTROKE;

#endif //(_WIN32_WINNT >= _WIN32_WINNT_WIN8)
    

让我们看看为 .Net 和 C# 重定义的结构。

struct XInputGamePad

游戏手柄有两种类型的数据可供显示;来自模拟输入的信息和来自数字输入的信息。数字输入包括 A、B、X、Y、LB 和 RB 按钮。它还包括 Start、Back、D 键的四个方向以及拇指摇杆下方的按钮。模拟输入包括两个扳机和两个拇指摇杆。对于模拟输入,每个输入都有一个字段。左触发器和右触发器都是一个介于 0 到 255 之间的字节整数值。拇指摇杆以介于 -32,768 和 32,767 之间的 16 位整数形式返回其 X 和 Y 值。所有数字输入都返回在一个名为 wButtons 的单个字段中。

[StructLayout(LayoutKind.Explicit)]
public struct  XInputGamepad
{
    [MarshalAs(UnmanagedType.I2)]
    [FieldOffset(0)]
    public short wButtons;

    [MarshalAs(UnmanagedType.I1)]
    [FieldOffset(2)]
    public byte bLeftTrigger;

    [MarshalAs(UnmanagedType.I1)]
    [FieldOffset(3)]
    public byte bRightTrigger;

    [MarshalAs(UnmanagedType.I2)]
    [FieldOffset(4)]
    public short sThumbLX;

    [MarshalAs(UnmanagedType.I2)]
    [FieldOffset(6)]
    public short sThumbLY;

    [MarshalAs(UnmanagedType.I2)]
    [FieldOffset(8)]
    public short sThumbRX;

    [MarshalAs(UnmanagedType.I2)]
    [FieldOffset(10)]
    public short sThumbRY;


    public bool IsButtonPressed(int buttonFlags)
    {
        return (wButtons & buttonFlags) == buttonFlags;
    }

    public bool IsButtonPresent(int buttonFlags)
    {
        return (wButtons & buttonFlags) == buttonFlags;
    }



    public void Copy(XInputGamepad source)
    {
        sThumbLX = source.sThumbLX;
        sThumbLY = source.sThumbLY;
        sThumbRX = source.sThumbRX;
        sThumbRY = source.sThumbRY;
        bLeftTrigger = source.bLeftTrigger;
        bRightTrigger = source.bRightTrigger;
        wButtons = source.wButtons;
    }

    public override bool Equals(object obj)
    {
        if (!(obj is XInputGamepad))
            return false;
        XInputGamepad source = (XInputGamepad)obj;
        return ((sThumbLX == source.sThumbLX) 
        && (sThumbLY == source.sThumbLY)
        && (sThumbRX == source.sThumbRX)
        && (sThumbRY == source.sThumbRY)
        && (bLeftTrigger == source.bLeftTrigger)
        && (bRightTrigger == source.bRightTrigger)
        && (wButtons == source.wButtons)); 
    }
}

IsButtonPressed 方法接受一个标识感兴趣按钮的常量,并在 wButtons 成员中查找该按钮是否被按下,并相应地返回 truefalse。我添加了一些方法使该结构更易于使用。IsButtonPressedIsButtonPresent 方法具有完全相同的实现。查询控制器状态和控制器功能时使用相同的结构。我添加了这两个方法作为一种记法上的区别。

XInputVibration

XInputVibration 结构包含两个介于 0 到 65,535 之间的无符号整数。放入结构中的值越高,关联的电机打开时的转速就越快。

    [StructLayout(LayoutKind.Sequential)]
public struct  XInputVibration
{
    [MarshalAs(UnmanagedType.I2)]
    public ushort LeftMotorSpeed;

    [MarshalAs(UnmanagedType.I2)]
    public ushort RightMotorSpeed;
}

XInputState

XInputState 结构是 XInputGamepad 结构的一个扩展,增加了一个 PacketNumber 字段。当控制器状态被查询几次后,如果自上次查询以来控制器状态保持不变,则此值将保持不变。如果控制器状态发生变化,则此值将不同。

[StructLayout(LayoutKind.Explicit)]
public struct  XInputState
{
        [FieldOffset(0)]
    public int PacketNumber;

        [FieldOffset(4)]
    public XInputGamepad Gamepad;

        public void Copy(XInputState source)
        {
            PacketNumber = source.PacketNumber;
            Gamepad.Copy(source.Gamepad);
        }

        public override bool Equals(object obj)
        {
            if ((obj == null) || (!(obj is XInputState)))
                return false;
            XInputState source = (XInputState)obj;

            return ((PacketNumber == source.PacketNumber)
                && (Gamepad.Equals(source.Gamepad)));
        }
}

XInputCapabilities

XInputCapabilities 结构返回控制器的功能。并非所有 Xbox 控制器都具有所有可用按钮,例如跳舞垫通常只包含。有趣的是,Type 字段始终会填充相同的值(将来可能会改变),目前可以忽略。感兴趣的字段是 SubType 字段。您可以判断控制器是游戏手柄、街机摇杆、方向盘还是其他类型的控制器。您可以在 子类型文档页面 上找到控制器类型的列表。我在 XInputConstants.cs 文件中添加了 ControllerSubtypes 枚举,其中包含此字段的可能值。控制器的其他功能由 Flags 成员指示。这包括控制器是否为无线、是否支持语音、是否具有导航按钮(开始、返回、方向键)、是否支持力反馈以及是否具有插入模块(如聊天手柄)。可能的值可以在 XInputConstants.cs 中的 CapabilityFlags 枚举中查看。

下一个字段是 XinputGamepad。对于每个可能的输入,如果控制器支持该输入,则会有一个非零值。XInputGamepad 结构之后是 XInputVibration 结构。与 XInputGamepad 一样,如果支持左右电机,则此结构中的字段将具有非零值。
[StructLayout(LayoutKind.Explicit)]
public struct  XInputCapabilities
{
    [MarshalAs(UnmanagedType.I1)]
    [FieldOffset(0)]
    byte Type;

    [MarshalAs(UnmanagedType.I1)]
    [FieldOffset(1)]
    public byte SubType;

    [MarshalAs(UnmanagedType.I2)]
    [FieldOffset(2)]
    public short Flags;

        
    [FieldOffset(4)]
    public XInputGamepad Gamepad;

    [FieldOffset(16)]
    public XInputVibration Vibration;
}

XInputBatteryInformation

对于使用电池的控制器,此结构将指示控制器使用的电池类型并指示电池电量。支持的电池类型为 NiMH、碱性、未知、已断开连接和有线(对于不使用电池的设备)。对于电池电量,仅支持 4 个值来指示空、低、中和满。这两个字段的值也在 XInputConstants.cs 文件中的 BatteryTypeBatteryLevel 枚举中定义。

[StructLayout(LayoutKind.Explicit)]
public struct  XInputBatteryInformation
{
    [MarshalAs(UnmanagedType.I1)]
    [FieldOffset(0)]
    public byte BatteryType;

    [MarshalAs(UnmanagedType.I1)]
    [FieldOffset(1)]
    public byte BatteryLevel;
}

XInput 函数

我从 XInput 库中使用了四个函数来与控制器进行交互:XInputGetStateXInputSetStateXInputGetCapabilitiesXInputGetBatteryInformation。所有这些函数都接受要操作的控制器的索引以及存储或检索信息的结构作为参数。XInputGetBatteryInformationXInputGetCapabilities 这两个函数接受一个额外的参数。XInputGetBatteryInformation 接受一个设备类型的参数进行探测。如果用户正在使用无线耳机,它可能有自己的电池电量,与控制器本身的电量分开。BatteryDeviceType 枚举包含 BATTERY_DEVTYPE_GAMEPADBATTERY_DEVTYPE_HEADSET 的值。XInputGetCapabilities 只接受 INPUT_FLAG_GAMEPAD 的单个值作为其附加值。将来随着更多功能的暴露,此参数可能接受不同的值。

        [DllImport("xinput1_4.dll")]
        public static extern int XInputGetState
        (
            int dwUserIndex,  // [in] Index of the gamer associated with the device
            ref XInputState pState        // [out] Receives the current state
        );

        [DllImport("xinput1_4.dll")]
        public static extern int XInputSetState
        (
            int dwUserIndex,  // [in] Index of the gamer associated with the device
            ref XInputVibration pVibration    // [in, out] The vibration information to send to the controller
        );

        [DllImport("xinput1_4.dll")]
        public static extern int XInputGetCapabilities
        (
            int dwUserIndex,   // [in] Index of the gamer associated with the device
            int dwFlags,       // [in] Input flags that identify the device type
            ref XInputCapabilities pCapabilities  // [out] Receives the capabilities
        );


        [DllImport("xinput1_4.dll")]
        public static extern int XInputGetBatteryInformation
        (
              int dwUserIndex,        // Index of the gamer associated with the device
              byte devType,            // Which device on this user index
            ref XInputBatteryInformation pBatteryInformation // Contains the level and types of batteries
        );

XboxController 类

现在我已经介绍了 XInput 中所有可用的结构和函数,我还要介绍一个用于管理这些其他类使用的类。我已将对功能的调用包装在一个名为 XboxController 的类中。此类的构造函数是私有的。一个系统最多可以有 4 个 Xbox 控制器。因此,我限制了对构造函数的访问,以防止创建超过 4 个实例。要获取控制器实例,可以使用静态方法 XbocController.RetrieveController

需要轮询控制器以持续获取其状态更新。在 XNA 或 DirectX 程序中,这会是游戏循环的一部分,所以你真的不需要考虑它。如果在没有此类循环的环境中使用该类,有两种获取控制器更新的选项。你可以手动调用 UpdateState,也可以调用静态方法 XboxController.StartPolling。静态方法将创建一个新线程,该线程将以一定的频率更新控制器状态。默认情况下,我将其设置为每秒 25 次。如果你想更改为其他值,请将你希望接收的每秒更新次数赋值给静态成员 UpdateFrequency。当你不希望再收到更新时,请记住调用 XboxController.StopPolling 方法来结束线程。在我附在本篇文章的代码示例中,我在程序关闭时调用了 StopPolling 方法。

protected override void OnClosing(CancelEventArgs e)
{
    XboxController.StopPolling();
    base.OnClosing(e);
}

该类公开了一个名为 StateChanged 的事件,如果控制器的状态与上次调用时有所不同,就会触发该事件。此事件的事件参数包含控制器当前和之前状态。

要使控制器振动,请调用 Vibrate 方法。你可以通过传递两个双精度值(介于 0.0d 和 1.0d 之间)来调用此方法,以指示电机应该运转的速度。大于 1.0 的值将被限制为 1.0。你还可以选择指定电机运转的时间。如果未指定时间间隔,电机将一直运转,直到再次调用 Vibrate 并指定速度为零。Xbox 控制器类的轮询循环还会检查是否到了关闭电机的时间(如果指定了时间间隔)。

#region Motor Functions
public void Vibrate(double leftMotor, double rightMotor)
{
    Vibrate(leftMotor, rightMotor, TimeSpan.MinValue);
}

public void Vibrate(double leftMotor, double rightMotor, TimeSpan length)
{
    leftMotor = Math.Max(0d, Math.Min(1d, leftMotor));
    rightMotor = Math.Max(0d, Math.Min(1d, rightMotor));

    XInputVibration vibration = new XInputVibration() { LeftMotorSpeed = (ushort)(65535d * leftMotor), RightMotorSpeed = (ushort)(65535d * rightMotor) };
    Vibrate(vibration, length);
}
        

public void Vibrate(XInputVibration strength)
{
    _stopMotorTimerActive = false;
    XInput.XInputSetState(_playerIndex, ref strength);
}

public void Vibrate(XInputVibration strength, TimeSpan length)
{
    XInput.XInputSetState(_playerIndex, ref strength);
    if (length != TimeSpan.MinValue)
    {
        _stopMotorTime = DateTime.Now.Add(length);
        _stopMotorTimerActive = true;
    }
}
#endregion

最重要的是,XBoxController 有一个输入属性,以便你可以读取其状态。对于数字输入,调用其中一个方法间接导致调用 IsButtonPressed

#region Digital Button States
public bool IsDPadUpPressed
{
    get { return gamepadStateCurrent.Gamepad.IsButtonPressed((int)ButtonFlags.XINPUT_GAMEPAD_DPAD_UP); }
}

public bool IsDPadDownPressed
{
    get { return gamepadStateCurrent.Gamepad.IsButtonPressed((int)ButtonFlags.XINPUT_GAMEPAD_DPAD_DOWN); }
}

public bool IsDPadLeftPressed
{
    get { return gamepadStateCurrent.Gamepad.IsButtonPressed((int)ButtonFlags.XINPUT_GAMEPAD_DPAD_LEFT); }
}

public bool IsDPadRightPressed
{
    get { return gamepadStateCurrent.Gamepad.IsButtonPressed((int)ButtonFlags.XINPUT_GAMEPAD_DPAD_RIGHT); }
}

public bool IsAPressed
{
    get { return gamepadStateCurrent.Gamepad.IsButtonPressed((int)ButtonFlags.XINPUT_GAMEPAD_A); }
}

public bool IsBPressed
{
    get { return gamepadStateCurrent.Gamepad.IsButtonPressed((int)ButtonFlags.XINPUT_GAMEPAD_B); }
}

public bool IsXPressed
{
    get { return gamepadStateCurrent.Gamepad.IsButtonPressed((int)ButtonFlags.XINPUT_GAMEPAD_X); }
}

public bool IsYPressed
{
    get { return gamepadStateCurrent.Gamepad.IsButtonPressed((int)ButtonFlags.XINPUT_GAMEPAD_Y); }
}


public bool IsBackPressed
{
    get { return gamepadStateCurrent.Gamepad.IsButtonPressed((int)ButtonFlags.XINPUT_GAMEPAD_BACK); }
}


public bool IsStartPressed
{
    get { return gamepadStateCurrent.Gamepad.IsButtonPressed((int)ButtonFlags.XINPUT_GAMEPAD_START); }
}


public bool IsLeftShoulderPressed
{
    get { return gamepadStateCurrent.Gamepad.IsButtonPressed((int)ButtonFlags.XINPUT_GAMEPAD_LEFT_SHOULDER); }
}


public bool IsRightShoulderPressed
{
    get { return gamepadStateCurrent.Gamepad.IsButtonPressed((int)ButtonFlags.XINPUT_GAMEPAD_RIGHT_SHOULDER); }
}

public bool IsLeftStickPressed
{
    get { return gamepadStateCurrent.Gamepad.IsButtonPressed((int)ButtonFlags.XINPUT_GAMEPAD_LEFT_THUMB); }
}

public bool IsRightStickPressed
{
    get { return gamepadStateCurrent.Gamepad.IsButtonPressed((int)ButtonFlags.XINPUT_GAMEPAD_RIGHT_THUMB); }
}
#endregion

对于模拟输入,将返回左触发器或右触发器的数值,或者返回拇指摇杆的数值对(代表 X 和 Y 方向)。

#region Analogue Input States
public int LeftTrigger
{
    get { return (int)gamepadStateCurrent.Gamepad.bLeftTrigger;  }
}

public int RightTrigger
{
    get  {  return (int)gamepadStateCurrent.Gamepad.bRightTrigger; }
}

public Point LeftThumbStick
{
    get
    {
        Point p = new Point()
        {
            X = gamepadStateCurrent.Gamepad.sThumbLX,
            Y = gamepadStateCurrent.Gamepad.sThumbLY
        };
        return p;
    }
}

public Point RightThumbStick
{
    get
    {
        Point p = new Point()
        {
            X = gamepadStateCurrent.Gamepad.sThumbRX,
            Y = gamepadStateCurrent.Gamepad.sThumbRY
        };
        return p;
    }
}

#endregion

历史

  • 2011 年 11 月 11 日 - 首次发布

© . All rights reserved.