本日は aptpod Advent Calendar 2022の12日目、担当は開発本部のやべです。普段は EDGEPLANT 関係の開発業務をやっています。
さて、皆さん、スプラトゥーン3やってますか?12月から新シーズンが始まってますます楽しくなってきましたね。 aptpod には circle_switch なる slack のチャンネルがあるのですが、最近はもっぱらスプラトゥーンの話題です。先日も 8人集めてプライベートマッチやってました。
当初は最近調査していた Multipath TCP に関するきっちりした技術記事を書こうと考えていましたが、業務以外での社内の雰囲気も伝わるかと思い、このネタにしてみました。
目次はこんな感じです。イカ、よろしく~
※本記事ではところどころスプラトゥーン3のプレイ画像が挟まりますが、任天堂のガイドラインを考慮してモザイク処理を施しております。見づらいですがご容赦ください。
概要
今回の目標は、キーボード入力をサーバーを経由して、Switchにコントローラーの情報として送り、実際に操作するところまでになります。
まずはおおまかな構成について説明しておきます。
今回は手元にあった機材を流用して、よくある制御のパターンに近い構成にしています。
- 入力側:開発試作機(こちらで紹介しています)にキーボードを接続
- 制御側:Raspberry Pi 4B を Bluetoothコントローラーとして動作するように設定し、Switch からHDMI映像を入力する
実際の機材の接続は、こんな感じでした。
当初制御側のエッジでは Raspberry Pi 4B のUSBポートを直接利用していたのですが、それだと Bluetooth 接続時にHDMI入力が切れる問題が発生したため、セルフパワー給電ができるUSBハブを介することで動作を安定させています。写真に写っているのは StarTech 社のハブですが、同社の製品は信頼性が高く、aptpod でもよく採用しています。
環境構築編
データのやり取りには、aptpod の製品である intdash Edge を利用しています。利用方法などを知りたい方は、こちらをご覧ください。
入力側の準備
入力側では、以下のデータを取得してサーバーに送ります。
- キーボード入力
- カメラ映像(キーボードを映すもの)
カメラについてはデフォルトで intdash Edge がサポートしているため、取得設定を行うのみです。キーボード入力については、実装スピード重視で、Python のライブラリを利用しました。
キーボードの入力は、それぞれコントローラーの何らかのボタンに割り当てます。入力内容を変換する処理をどこでやるかは全体のシステム設計にもよりますが、データサイズを均一にするため整形を行ってサーバーに送りたいので、今回は入力エッジ側で変換するようにします。
CONTROLS = { "l": "BA", "k": "BB", "i": "BX", "j": "BY", ... }
このような形で、入力データを2文字のコントローラー用のコードに変換しました(例:BA = Button A)。このデータを、ループして push/release のタイミングで送っていきます。
while True: event = keyboard.read_event() if event.name == "esc": print("escape was pressed") break if not event.name in CONTROLS: continue ctrl = CONTROLS[event.name] if event.event_type == keyboard.KEY_DOWN: on_push(ctrl) else: on_release(ctrl)
on_push
、on_release
では、FIFO経由で、データ送信処理を管理しているモジュール(Edge Agent)に対してデータを送っています。
制御(ゲーム操作)側の準備
制御側では、以下のことを行います。
- キーボード入力をサーバーから受け取って、コントローラーとして動作する
- HDMI映像をサーバーへ送信する
HDMI入力は、安価なキャプチャデバイスを使ってUSB経由で入力します。設定自体は一般的なUSBカメラと同じもので動作します。コントローラーとしての動作については、こちらの Python のライブラリを利用しました。
入力側からは、コントローラーの入力に対応したコードが送られてくるため、制御側では実際の定義に置き換えるようにします。
BUTTONS = { "BA": nxbt.Buttons.A, "BB": nxbt.Buttons.B, "BX": nxbt.Buttons.X, "BY": nxbt.Buttons.Y, ... }
これらのデータが届いたタイミングで、対応するボタン入力(もしくはスティック入力)が動作するようにします。
while True: recv = fifo.read(18) if len(recv) != 18: continue d = decode(recv) if d["status"] == 0: # release button continue ctrl = d["ctrl"] if ctrl in BUTTONS: nx.press_buttons(controller_index, [BUTTONS[ctrl]]) ...
調整編
とりあえず作っては見たものの、入力に対して反応が悪くなイカ?
操作情報と一緒にCPU負荷を送っているのでそれを見てみると、制御側の負荷がかなり高くなっています。
おそらく入力を処理しきれていないため、かなり遅延が発生しているようです。そこでライブラリのコードを参考に、使うAPIの変更やキーボード入力の間引き、押し込み時間の調整など、いくつかの対策を行ったところ、かなり改善しました。
赤枠のCPU使用率を見てみると、負荷が40%程度軽減されています。
スペシャル(必殺技的なやつ)もしっかり打てるぞ!
精度はさておき、ここまででキーボード入力を受け取って操作を行うというタスクについては完了しました。
実戦編
というわけで、いざ実戦投入!
といっても、見知らぬ方々にご迷惑をおかけするわけにはいかないので、冒頭で話した社内プライベートマッチで一戦だけ行いました。
結果、瞬殺。
映像の遅延やボタン入力の反応はさほど悪くないものの、スティック側の入力反映が微妙で思ったように動きません。また、移動しながらの視点変更など、同時入力を必要とするケースを考慮していなかったのも、けっこう辛いポイント。実際に社内で制御を扱っているプロジェクトでは色々と考慮して設計されているわけで、遠隔制御は1日にしてならず、と痛感いたしました(みんなすごい!)。
おわりに
イカがだったでしょうか?intdash を利用した遠隔制御の雰囲気が伝わっていれば幸いです。
実際に自分でやってみた感想としては、単なる疎通まではたいして時間はかからないものの、その後の操作感の向上や安定性などを詰めていく作業が大変でした。ボードゲームのようなものならこれでも十分ですが、今回扱ったスプラトゥーンのように複雑な操作が必要なゲームに実戦レベルで対応するためには、更なる改善検討が必要になります。操作データの送信方法の見直しや処理の非同期化、Python以外の言語への置き換えなど、やれることはまだまだあります。
こんな記事もあるように、世の中的には遠隔制御とゲームの親和性も注目されていたりするので、今回やった取り組みは案外エントリーとしてはいいお題なのかな?とも思いました。
ほな カイサン!!!