どうしてもプロコンでマウス操作したい
動機
我が家にSplatoon 3のブームが訪れました。
最近のゲームはコントローラー内蔵のモーションセンサーを使って、対象に照準を合わせたりします。
モーションセンサーを用いて瞬発的に照準を合わせる行為(瞬間エイムと呼ばれています)を日々繰り返すうちに、これマウスより速いんじゃないかと思い始めたのが事の発端です。
既存のソフトウェアでも、いくつかプロコンをマウスとして扱えるようにするものはあったのですが、モーションセンサーに対応しているものは見つけられなかったのと、スティックの挙動も好みではなく、自分で書こうかなと思い至りました。
要件
今回のソフトウェア開発にあたり、以下の要件を定義しました。
カーソル移動がSplatoonの操作感に近しいこと
Splatoonのカーソル操作は、右スティックとモーションセンサーを組み合わせて行います。
右スティックで大まかな移動を行い、モーションセンサーで微調整する使い方が多いかと思います。
右スティックのカーソル移動は重みがあり、スティックを倒した直後にカーソルが最高速度になるわけでも、戻してすぐカーソルが止まるわけでもありません。
イメージとしてはエンジンブレーキが効いている車に対して、アクセルを踏む量をスティックで調整している感じです。
対象的にモーションセンサーについては、かなりダイレクトに手の動きがカーソル移動に変換されます。
またコントローラーをある程度斜めにした状態でも、水平に動かせばカーソルも水平に、鉛直に動かせばカーソルも鉛直に動きます。
マウスとしての機能が一通り揃っていること
一般的なマウスに備わっている、左右クリック・ホイール・ホイール押下は必須とします。
Google Chromeで行う標準的な作業を一通りこなせること
戻る・進む・タブの切替など、一般的な操作はプロコンだけで完結できることとします。
仕様
上記の要件に対し、以下の仕様を定義しました。
プロコン自体の仕様
ソフトウェア仕様を考える手前で、そもそもプロコンの仕様を整理します。
- PCとの接続はBluetoothで行う
- ジャイロセンサー
- どの方向にどの程度の速度で回転しているかを数値化できます。
- 加速度センサー
- どの方向にどの程度加速度が働いているかを数値化できます。
- 静止状態では重力加速度のみ働いているため、地面に対してどのような姿勢になっているかを計算できます。
- 押下可能なアナログスティックが2本
- ボタンがいっぱい
一般的にはジャイロセンサーと加速度センサーをひっくるめてモーションセンサーと呼ばれている気がします。
モーションセンサーが概念的なもので、ジャイロ・加速度センサーはその実装という感じがしますね。
スティックを倒した量の解釈
スティックを倒した量は、縦軸・横軸それぞれ数値として取得できます。
その数値をそのまま速度として解釈すると、滑らかさが失われてしまうため、加速度として扱います。
既存のソフトウェアは、スティックの数値をそのまま速度として解釈しているものが多い印象を受けました。
モーションセンサの解釈
コントローラーの回転系をオイラー角で表すと、以下のようになります。
オイラー角の文脈でプロコンを見ると、ステルス機に見えてくる。 |
ジャイロ・加速度センサーどちらも、この3軸に対する値を取得することができます。
基本的にはPitchの角速度がカーソルの縦軸速度に、Yawの角速度が横軸速度に変換されれば良さそうです。
スティックは滑らかさを重視してセンサーの入力値を加速度として扱いましたが、モーションセンサーはダイレクトさを重視して、そのまま速度として扱います。
コントローラーが斜めになった状態の解釈
コントローラーが斜めになっている状態というのは、上図のRollが変化している状態と解釈できます。
Roll方向にもジャイロセンサーが入っているので、その積分値が現在のRoll値ということができそうです。
Roll値が定まれば、前述のPitchとYawの角速度に対してRoll値に応じたsin, cos変換を行うことで、回転された状態を加味して縦軸・横軸への変換を行えそうです。
基本的には上記の計算で良いのですが、実はジャイロセンサーの積分のみでRoll値を求めると、少しずつ角度がずれていってしまいます。
これはセンサー精度の問題で、何もしなくてもジャイロセンサーから微量の値は入ってくるし、同じ量右に回して左に戻しても、差し引き0にはならないものです。
対応として、加速度センサから算出可能な、地球に対してのRoll値を用いて値を補正します。
加速度センサーは上図の各軸に対して、どの程度の加速度(この場合は重力加速度)が加わっているかを値として取得できるため、YawとPitchの値に対してarctanを適用してやれば、地球に対するRoll値が求まりそうです。
なお加速度センサーの入力値は、コントローラーが動かされている際はその動きの加速度の影響も受けてしまい、地球に対してどうかの計算値が乱れてしまうため、実際にはかなり値を薄めて、ゆっくり適用していきます。
ここらへんの話題は、ロボットの姿勢制御の分野で耳にすることが多いかと思います。
興味のある方は調べたら面白いと思います。
マウスの基本機能やGoogle Chrome用のショートカット
プロコンには潤沢な量の入力があるため、特に割当に困ることは無さそうです。
実装方針の検討
dekuNukemさんが解析結果を公開しているため、色々な既存の資源がありました。
ブラウザベース
自分が最も触る環境なので、まずチェックしてみました。
WebHID APIやGamepad APIなど、普段業務で触らない機能が実はあったんだなと驚きました。
WebHID API上で実装されたドライバもあったりしました。
とはいえ、ブラウザからはOSのマウス操作は難しいのと、メモリの消費量も大きいため、今回は見送りました。
Node.js
こちらも自分が最も触る環境なので、チェックしてみました。
実はブラウザベースの技術を検討していた際、Node.jsで立ち上げたサーバーとブラウザのフロントエンドをWebSocketでつないで、Node.jsからマウスを操作しようかなと迷走していた瞬間もありました。
Node.js上で走るジョイコンのドライバが素直に走らなかったり、そもそもHIDとの通信はネイティブモジュールでやってるわけで、Node.jsのオーバヘッド大きすぎだなという結論に至り結局見送ったのですが、検討の途中で試してみたオートメーションツールのnut.jsはかなり面白かったです。
機会があれば使いたいなあと思いました。
C/C++
一番既存の資源が多かった気がします。
ただWindowsでしか試してないよとかそういうライブラリが多かったり、そもそもC/C++触りたくないなあと思って見送りました。
Rust
Rustの資源もあり、ちょっと試してみたら非常に使いやすかったので、Rustで実装することにしました。
Yamakaky/joyをベースに実装させていただきました。
今回作るものがドライバなので、メモリ使用量やパフォーマンスも気にするところではありますが、Rustはその点かなり有利なのも選定の理由です。
実際に書いてみた結果
今回始めてRustを書いてみたのですが、開発環境のセットアップもコマンド一発ででき、クローンしてきたリポジトリもcargoでパッとビルドでき、開発者体験めちゃいいなあというのが第一印象でした。
仕様時点では見えておらず、実装時にガチャガチャしたところ
コントローラーとの通信頻度が意外と少ない
当初の実装では1ループ内で通信完了→マウス動かすを回していたのですが、これだとマウスがカクつくことがありました。
実行状況を眺めると、コントローラーとの通信が長い時は30msくらい間が空いているようでした。
通信上の仕様なのかライブラリの実装なのか、なんとなくライブラリ側な気がしましたが、対症療法としてマウスを動かすスレッドと通信された最終値を保存するスレッドを分離したら通信の時間を体感できなくなってしまったので、それ以上調査しないでいます。
1ピクセル以下の画面に反映できなかった動きを積分しておく必要があった
マウスを動かす単位は1ピクセルが最小値なのですが、本来動かしたい量は1ピクセルに満たないことがままありました。
最初は1ピクセル以下の単位は四捨五入して利用していたのですが、それだとゆっくり斜めに動かした際など、不自然にまっすぐ縦か横にしかカーソルが移動しない挙動となってしまいました。
画面に反映できなかった小数以下の移動は積分しておき、積分値が画面に反映できる量になったタイミングで、画面に反映しつつ反映分を積分値から差し引きする実装にすることで、上記の問題を解決できました。
Mac OSのキー入力イベントが意外と魔境
キー入力イベントがOSに発火されてから、実際に効力を発揮するまで「しばらく」かかるようで、ライブラリのコード内でも20ms待機する実装がおこなわれていました。
しかも20msは低スペックマシンだと十分ではないようで、自身のコード上は論理的にキーが押されっぱなしになることはないはずなのですが、実際には順番が前後することがある状態となってしまいました。
つまり発火した順番と効力を発揮する順番が担保されてないため、キー入力に関しては何をしてもRace Conditionが発生する魔境になっているようでした。
対応として、クリティカルなキー入力の切れ目では「十分な時間(100ms)」sleepすることで、順番を担保してみました。
成果
感想
Rustを使ってみて何より感動したのは、コンパイラがとても親切なことでした。
型システムの強さに比例してコンパイルエラーの解釈が難解になっていくのは避けられないため、他の型システムが強力な言語との単純比較は不平等ですが、それにしても分かりやすかったです。
コンパイラがなんでも教えてくれるので、適当にガチャガチャやってたら段々言語仕様が分かってきます。
クロスコンパイルもコマンド一発でできてしまい、Rust大好きになってしまいました。