C# でゲームパッドの入力を処理しよう！

#概要・背景
本記事では Vortice.XInput ライブラリを使ったゲームパッドの入力処理について解説します。

素の C# アプリケーションが対象となります。Unity 等のゲーム開発用フレームワーク上で動くプログラムの場合は、フレームワーク独自のやり方が用意されています。それらは本記事の対象ではないという意味です。

私自身のゲームパッド入力処理実装経験は今回が初めてです。
個人アプリでの利用経験を元に本記事を書いています。.NET Framework 4.8 のアプリとして実装した経験となります。

一般的なやり方から外れている部分があるよといったご指摘等もお待ちしております。

#Vortice.XInput とは？
Vortice.Windows という DirectX 系 API の .NET 上での利用をサポートしてくれるライブラリの一部です。
同じ役割をしていた SharpDX というライブラリが有名だったようなのですが開発が滞り 2019 年に凍結された模様です。

Vortice.Windows はその後継として作られているライブラリです。SharpDX では使っていなかった新しい技術によって作られているみたいです。

Vortice.Windows のサイトには Unsafe や SharpGen.Runtime などの最新の技術を使っていると書いてあります。
これらの技術はアンマネージドコードをうまく使うためのものだと思います。（主にメンテナンスの面で）SharpDX よりスマートな実装になっていると想像できますね。

凍結されたとはいえ SharpDX をまだ使うという方法もありそうですが、個人アプリ開発目的ですので無茶ができますし、初のゲームパッド入力処理実装ということもありどうせゼロから覚えるので新しい Vortice.XInput の利用を試みることにしました。

使った感想は「めちゃめちゃ簡単だった」です。
#Vortice.XInput でできることって？
以下のようなことができます。

- ゲームパッドの機能取得（XInput.GetCapabilities）
- ゲームパッドのバッテリー情報の取得（XInput.GetBatteryInformation）
- ゲームパッドの状態の取得（XInput.GetState）
- ゲームパッドのキーストロークの取得（XInput.GetKeystroke）
- ゲームパッドのバイブレーション制御（XInput.SetVibration）

本記事では XInput.GetState と XInput.GetKeystroke について解説します。使ったのがこの２つだからです。

# 解説
## 共通事項
### Vortice.XInput を使うには？
NuGet で Vortice.XInput をプロジェクトに追加してください。

ライブラリの名前空間は「Vortice.XInput」です。利用するプログラムには以下の一文を入れましょう。

```C#
using Vortice.XInput;
```

機能は Vortice.XInput.XInput クラスに集約されているようです。
static クラスですのでインスタンス化の必要もなく、直接メソッドを呼び出して使います。
機能を使うための事前処理は何もなく、いきなり呼び出して大丈夫です。

### userIndex について
XInput の全てのメソッドは userIndex を受け取ります。userIndex はゲームパッドのインデックスで、0 ～ 3 の値を指定します。

私の環境では作り始めの際はとりあえず 0 を指定して動いたのでしばらくはそれで開発を進め、のちに XInput.GetState を使った userIndex の自動認識＆選択UIの実装を行いました。

userIndex の自動認識については XInput.GetState にて解説します。

### System.Timers.Timer と組み合わせて使う
ゲームパッドの入力を非同期で取得して使うために System.Timers.Timer を使います。
以下は 10 ミリ秒置きに処理する例です。

```C#
using System;
using System.Timers;

namespace ExampleApp
{
    public class GamepadTimer
    {
        private Timer Timer { get; set; }
        public int Interval { get; set; } = 10;  // 10 ミリ秒

        protected object Lock { get; } = new object();

        public void Start()
        {
            lock (Lock)
            {
                Stop();

                Timer = new Timer(Interval);
                Timer.Elapsed += Elapsed;
                Timer.Start();
            }
        }

        public void Stop()
        {
            lock (Lock)
            {
                if (Timer?.Enabled == true)
                {
                    Timer?.Stop();
                    Timer = null;
                }
            }
        }

        private void Elapsed(object sender, EventArgs e)
        {
            if (Monitor.TryEnter(Lock))
            {
                try
                {
                    // ここにゲームパッドの処理を書く
                }
                finally
                {
                    Monitor.Exit(Lock);
                }
            }
        }
    }
}
```

共通事項は以上になります。

ここからが本題となる具体的な機能の話です。

## ゲームパッドの状態の取得（XInput.GetState）
XInput.GetState で現在のゲームパッドの状態を取得できます。

利用例は以下の通りです。

```C#
if (XInput.GetState(0, out var keystate))
{
  if (0 < (keystate.Gamepad.Buttons & GamepadButtons.A))
  {
    // A ボタンを押していたら
  }
}
```

XInput.GetState の第一引数は userIndex です。

以上の例は「0 のゲームパッドが有効かつ A ボタンを押していたら」という条件を実現する例です。

XInput.GetState メソッドは呼び出した時点で指定した userIndex のゲームパッドが有効なら true、無効なら false を返すという特性を持っており、有効ならその時点のボタンやスティックの状態を取得できます。

有効状態を返すという特性を利用して、ゲームパッドの自動認識に使えます。例えば以下のようなコードです。

```C#
public class GamepadDetector
    public int? GamepadIndex { get; set; }

    public State? GetKeystate()
    {
        // 認識済みの場合、認識済みのゲームパッドを使う
        if (GamepadIndex != null)
        {
            if (XInput.GetState(GamepadIndex.Value, out var keystate))
                return keystate;
            else
                // 認識済みのゲームパッドが無効になったとみなす
                GamepadIndex = null;
        }
        else
            // 未認識の場合、0 ～ 3 の順で有効なゲームパッドを探す
            for (var i = 0; i < 4; ++i)
                if (XInput.GetState(i, out var keystate))
                {
                    GamepadIndex = i;
                    return keystate;
                }

        return null;
    }
}
```

GamepadDetector.GamepadIndex に UI で選択した値を入れることもできます。

XInput.GetState はその時点の正確な状態を取得できます。
アクション／シューティングゲームの自機操作のような連続的な処理に向きます。
一方でロールプレイングゲームでの選択肢の変更のような断続的な処理には向きません。
その場合に使えるのが次に紹介する XInput.GetKeystroke です。

## ゲームパッドのキーストロークの取得（XInput.GetKeystroke）
XInput.GetKeystroke でゲームパッドの「ボタンを押した」「ボタンを離した」というキーストロークイベントを取得できます。

利用例は以下の通りです。

```C#
if (XInput.GetKeystroke(0, out var keystroke))
{
  if (keystroke.Flags == KeyStrokeFlags.KeyDown && keystroke.VirtualKey == GamepadVirtualKey.A)
  {
    // A ボタンを押したら
  }
}
```

「ボタンを押した」を表す KeystrokeFlags.KeyDown と対になる「ボタンを離した」を表す KeystrokeFlags.KeyUp もあります。

また、ボタンを押しっぱなしにした場合を表す KeystrokeFlags.Repeat というものもあります。
KeystrokeFlags.Repeat は最初の KeystrokeFlags.KeyDown から少し間を開けてから始まり、そこから断続的な入力として受け取れます。
メモ帳とかでキーボードのボタンを押しっぱなしたときの入力と同じ感じです。

利用例は以下の通りです。

```C#
if (XInput.GetKeystroke(0, out var keystroke))
{
  if (0 < (keystroke.Flags & KeyStrokeFlags.Repeat) && keystroke.VirtualKey == GamepadVirtualKey.A)
  {
    // A ボタンを押しっぱなしにしたら
  }
}
```

また、スティック操作も上下左右斜めの８方向のキー操作として受け取れるというのも特徴的です。

## XInput.GetState と XInput.GetKeystroke を組み合わせたボタン同時押し判断の応用例
XInput.GetState と XInput.GetKeystroke を組み合わせてボタン同時押しの判断も簡単に作れました。

XInput.GetKeystroke のイベントをトリガーに XInput.GetState で同時押しを判断するといった感じです。

例は以下の通りです。

```C#
if (XInput.GetKeystroke(0, out var keystroke) && XInput.GetKeystroke(0, out var keystroke))
{
    if (keystroke.Flags == KeyStrokeFlags.KeyDown && keystroke.VirtualKey == GamepadVirtualKey.A
        && 0 < (keystate.Gamepad.Buttons & GamepadButtons.RightShoulder))
    {
        // RB を押しながら A ボタンを押したら
    }
}
```
# まとめ
以上、Vortice.XInput を使ったゲームパッドの入力処理の方法について解説しました。

Vortice.XInput でゲームパッドを扱うプログラムがめちゃめちゃ簡単に作れます。