C#でDirectInputを使いたいという需要、無さそう。備忘録。
・そも、DirectInputとは
MicrosoftによるDirectXというコンポーネントの一部で、入力デバイスからの情報を受けるAPIである。DirectXには後にXboxコントローラ用のXInputというAPIが追加されているので、これがたまに混乱を招いている (両対応のコントローラなどもある)。後発ということもあってXInputの方が実装はかなり簡単なのだが、後発であるがゆえに対応していないデバイスもあり、DirectInputに頼らざるを得ないケースがある。Wikipediaによれば「過去の技術とみなされている」らしい。本当か?
・Vorticeとは
もともとC#でDirectXを扱うオープンソースのライブラリにSharpDXというものがあった。DirectXを使うには基本的にネイティブのC++のコードを書く必要があるのだが、それをマネージドなC#で書けるようなラッパ(wrapper)を提供してくれていた。が、2019年に開発中止が発表され、後継としてSharpDXをforkしたVortice.Windowsというライブラリがこれまたオープンソースで開発されている。Vortice.DirectInputはそのライブラリのうちDirectInputを使うための部分である。
・ざっくりとした流れ
DirectInputはあまり洗練されていないというか、かなり回りくどいコードを書かないと使えないので、いったんDirectInputの公式のドキュメントを眺めてみることにする。これによると:
IDirectInput8
インタフェースを実装したオブジェクトを生成するIDirectInput8::EnumDevices
メソッドを用いて、有効なデバイスの一覧を取得するEnumDevices
で得られたデバイスのGUIDを用いて、デバイスのインスタンス (IDirectInputDevice8
インタフェースを実装している) を生成する- デバイスインスタンスのCooperative Levelを設定する
- デバイスインスタンスのデータフォーマットを設定する
- デバイスインスタンスのプロパティを設定する (バッファのサイズなどをここで指定する)
- デバイスインスタンスの
Acquire
メソッドを用いて、デバイスへのアクセスを取得する
といった流れである。ちょくちょくよくわからない用語が出てきたが、ここからはコードを眺めながら理解することにする。
・セットアップのコード
ここではJoystickを対象にしてみよう。using Vortice.DirectInput;
ディレクティブは忘れずに書いておこう。
public void InitializeDirectInput()
{
// setup directinput...
IDirectInput8 dinput = DInput.DirectInput8Create();
IList<DeviceInstance> devices = dinput.GetDevices(DeviceType.Joystick, DeviceEnumerationFlags.AttachedOnly);
if (devices.Count == 0)
{
return;
}
Guid guid = devices.First().InstanceGuid;
IDirectInputDevice8 device = dinput.CreateDevice(guid);
/*
Console.WriteLine(device.Capabilities);
IList<DeviceObjectInstance> obj = device.GetObjects();
foreach (DeviceObjectInstance objinstance in obj)
{
Console.WriteLine(objinstance.Name);
}
*/
device.SetCooperativeLevel((IntPtr)device, CooperativeLevel.NonExclusive | CooperativeLevel.Background);
device.SetDataFormat<RawJoystickState>();
device.Properties.BufferSize = 1024;
device.Acquire();
}
前節の流れと対照させながら見ていくと、1.が4行目に、2.が5行目に対応している (このライブラリではメソッド名がEnumDevices
ではなくGetDevices
になっているが)。
GetDevices
メソッドにはDeviceType
のフラグとDeviceEnumerationFlags
のフラグを渡すことができ、前者はその名の通り列挙するデバイスのタイプ(キーボード、マウス、Joystick、etc.)を指定できる。全部列挙するときはDeviceType.Device
にすればよい。後者は接続されているデバイスのみを列挙したり、エイリアスを含めたりするなどのオプションを指定できる。全部列挙するときはDeviceEnumerationFlags.AllDevices
を使う。
このへんの列挙型はネイティブの構造体をそのままマップしているようなので定義を発見するのは困難なのだが、IDE (だいたいVisualStudioでしょう) の補完でそれっぽい値を見つけることができる。
あとは3.が12-13行目に、4.~7.は24~27行目にそれぞれ対応する。
Cooperative Levelというのはざっくりした理解としては入力をどのくらい専有するかの程度だと思えばよい。基本的には、Exclusive↔Non-Exclusiveと、Background↔Foregroundの2つの設定が存在するので、可能な組み合わせは4通りである。Exclusiveというのは排他的という意味であって、自分以外のアプリケーションには入力が流れなくなる。つまり逆にNon-Exclusiveでは入力は他のアプリケーションと共有できる。Backgroundは文字通り自分が背景にいる (フォーカスを持っていない) ときにも入力を取ることができる。Foregroundでは自分にフォーカスがあるときしか入力が取れない。
デフォルトではNon-ExclusiveとBackgroundの組み合わせになっている。ウィンドウが非アクティブになっているときに入力が処理されると嫌だという場合はForegroundにすればよい。また、自分以外のアプリケーションに入力を渡したくないときはExclusiveにすればよい。ただしExclusiveとBackgroundの組み合わせにすると常時自分だけが入力を取れる状態になってしまうので、あまりお勧めしない (というかキーボードとマウスに関してはこの設定にすることは不可能である)。
データフォーマットを設定するところのSetDataFormat<T>
メソッドはジェネリックになっていて、<T>
の部分に (rawの) データフォーマットのクラスを指定する。今回はJoystickなのでRawJoystickState
になっている。キーボードとマウスならそれぞれRawKeyboardState
, RawMouseState
になる。
ところで間にコメントアウトしている部分が挟まっているのが気になる人もいるかもしれないが、これはデバイスに備えられている入力の一覧 (アナログスティック、ボタン、etc.) を列挙するコードになっている。はじめてDirectInputを扱うというときは、これでどういうタイプのデバイスにどういう入力が備わっているのか見てみてもよいだろう。
・入力を取る
さて、実際に入力を取ってみることにしよう。入力を取るうえでは、大きく分けて2つの方法がある。1つ目は、現在の状態を逐一取得する方法、2つ目は、入力イベントのバッファを取得する方法である。
1つ目は明快であろう。例えば次のようなコードを書けばJoystickのボタン10が押されているかどうかがすぐにわかる。
JoystickState state = device.GetCurrentJoystickState();
Console.WriteLine(state.Buttons.GetValue(10));
変数state
の中にはデバイスのあらゆる入力の状態が含まれているから、任意のボタン(など)について状態を取得することができる反面、明らかに要らないデータが多すぎる。ある1個のボタンについての情報が欲しいのに、(内部的には)128個のボタンと、アナログスティックと、その他もろもろの入力の情報まで毎回セットで付いてくるのだから無駄もいいところである。それに入力の状態が変化したかどうか確かめたいときにわざわざ前回の状態と比較しないといけないのもネックになる。
そこで登場するのが2つ目の方法である。これはデバイスの入力が変化したらそのイベントの情報をバッファに突っ込んでおき、欲しいときにそれを取り出すという方法である。コードは次のようになる。
JoystickUpdate[] updates = device.GetBufferedJoystickData();
foreach (JoystickUpdate u in updates)
{
if (u.Offset == JoystickOffset.Buttons10 && u.Value > 0)
{
// ボタン10が押されたときの処理
}
}
JoystickUpdate
オブジェクト(ここでは変数u
)の中にはOffset, Value, Timestamp
などの値が格納されている。Offset
はどの入力に変化があったかを、Value
はその変化後の値を示す。したがって最小限の情報で状態の変化を得ることができる。基本的にはこちらを使うことになるだろう。
・余談: 定期的な入力の取得
DirectInputそのものについてはこれでざっと終わりなのだが、大概こういう入力の処理では非同期で一定時間ごとの処理とは切っても切り離せないのがお決まりである。やれThreadだのTaskだのと非同期処理はほとほと苦手なので、ここはひとつRx (Reactive Extention)で簡潔にキメることにする (using System.Reactive.Linq;
ディレクティブが必要)。
IObservable interval = Observable.Interval(TimeSpan.FromMilliseconds(10));
IDisposable subscription = interval.Subscribe((_) =>
{
// 処理
});
たったこんだけである。簡単すぎてあっけに取られてしまったが、ざっくり説明すると、Observable.Interval
というのが指定した時刻(ここでは10 ms)ごとに「通知」を発生させる"Observable"である。Observableには日本語の定訳が存在してなさそうなので「観察対象」とでもしておこう。そしてObservableのSubscribe
の中にあるのが(概念的には) "Observer" =「観察者」である(ここではObserverは匿名なので、正確にはラムダ式そのものはObserver.onNext
メソッドである)
ObserverはObservableをSubscribe = 「購読」することでObserverからの「通知」を受け取り、そのたびに処理を行う。「観察」を止めたければSubscribe
メソッドの返り値のIDisposable
(ここではsubscription
)のDispose
メソッドを呼ぶだけでよい。詳しいことはRx専門の記事を見ていただきたいが、とにかくありえないほど簡潔に書けるので使えると幸せになれそうである。
いまどきC#を使ってる人なんかいるのでしょうか