Switchであそぼう

こちらは[クラスター Advent Calendar 2022](https://qiita.com/advent-calendar/2022/cluster)（1ページ目）の17日目の記事です。
昨日は @Hashimoto_Mami さんの「[ボクセル作成ツール「MagicaVoxel」を活用して3Dモデリングをしてみよう！](https://creator.cluster.mu/2022/12/16/magicavoxel/)」でした。ボクセルは手軽に作れる上に味があって良いですよね。

## はじめに

こんにちは、スワンマンです。クラスター株式会社でディレクターとかカスタマーサポートとかスワンマンをしています。
今回のAdvent Calendarではすでに「[Slackで同じワークフローのショートカットを複数チャンネルに設置する](https://qiita.com/Swanman/items/c7ebc94f7ee5e2418d00)」と「[Unityのエディタ拡張で動的にメニューを追加・削除する](https://qiita.com/Swanman/items/279b3b679f3f96a5f925)」という2つの記事を書いてるんですが、ちょっと中身が堅すぎたので今回は肩の力を抜いてSwitchで遊んでいきたいと思います！

## ところが両の手にはVRのコントローラが！

![手が塞がる問題](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/58380/f4f11611-54dc-846a-2944-8fab8698c691.png "手が2つしかないバグ")

なんてことだ…
人間の手は2つ…
別のコントローラをさらに持つことはできない…
構造的欠陥…

ということで今回は神による設計ミスのワークアラウンドとして、このコントローラを使ってSwitchで遊ぶ方法を考えたいと思います。

## VRデバイスの入力を取得する

[OpenVR](https://github.com/ValveSoftware/openvr)を使うとVRデバイスの入力を簡単に取れると聞いたので、今回はこれを使ってみます。
[headers](https://github.com/ValveSoftware/openvr/tree/master/headers)の中に「openvr_api.cs」があるので、こいつをC#のプロジェクトに取り込んで、[bin/win64](https://github.com/ValveSoftware/openvr/tree/master/bin/win64)の「openvr_api.dll」をフォルダに配置したら以下のようなコードでざっくり入力が取れます。
（これはLegacy APIらしいけど、今回は遊べればいいのでよしとする）

```csharp
EVRInitError error = EVRInitError.None;
OpenVR.Init(ref error, EVRApplicationType.VRApplication_Overlay);

// 左手コントローラの状態を取る
var leftHandIndex = OpenVR.System.GetTrackedDeviceIndexForControllerRole(ETrackedControllerRole.LeftHand);
VRControllerState_t leftControllerState = default;
TrackedDevicePose_t leftTrackedDevicePose = default;
OpenVR.System.GetControllerStateWithPose(ETrackingUniverseOrigin.TrackingUniverseSeated, leftHandIndex, ref leftControllerState, (uint)Marshal.SizeOf(typeof(VRControllerState_t)), ref leftTrackedDevicePose);

// ulButtonPressedに各ボタンの押下状態がビットフラグで入るのでいい感じに取る
if ((leftControllerState.ulButtonPressed & 1ul << (int)EVRButtonId.k_EButton_A) != 0)
{
    ...
}

// スティックのアナログ入力はleftControllerState.rAxis0に-1.0～1.0で入ってる
// xは左が-1.0で右が1.0
// yは下が-1.0で上が1.0
...

// 右手も同様に
...
```

## 入力をSwitchに送る

世の中にはかしこい人がいるので、[NXBT](https://github.com/Brikwerk/nxbt)というライブラリを使うとPCに生やしたBluetoothアダプタをコントローラとしてSwitchに認識させることができます。すごい。
Windows上で使うには仮想環境にUbuntuを入れてその中で立ち上げる必要があるんですが、vagrantを使って簡単にセットアップする仕組みが同梱されており、順調に進めば恐らく30分もかからずに接続までいけます。
（僕は運悪く非対応Bluetoothアダプタを使っていたせいで3時間くらい無駄に格闘した果てにAmazonで別のBluetoothアダプタを買いました…）

普通のゲームコントローラを使うだけであれば自身でコードを書くまでもなく使えてしまうんですが、今回は自前で取得した入力データを送りたいので以下のような感じのサーバーを書きます。
（送り手はこの[DIRECT_INPUT_PACKET](https://github.com/Brikwerk/nxbt/blob/27451e61b4d83b38230d42c3aec9129e656a18ce/nxbt/nxbt.py#L27)に入力値を詰めてjsonとして送ればOK）

```python
from http.server import HTTPServer
from http.server import BaseHTTPRequestHandler
import json
import nxbt

def run():
    nx = nxbt.Nxbt()
    controller_idx = nx.create_controller(nxbt.PRO_CONTROLLER)
    nx.wait_for_connection(controller_idx)
    
    class Handler(BaseHTTPRequestHandler):
        def log_message(self, format, *args):
            pass
        
        def do_PUT(self):
            content_len = int(self.headers.get('content-length'))
            recv_data = json.loads(self.rfile.read(content_len).decode('utf-8'))
            nx.set_controller_input(controller_idx, recv_data)
            self.send_response(204)
    
    server = HTTPServer(('0.0.0.0', 8000), Handler)
    server.serve_forever()

if __name__ == '__main__':
    run()
```

## 試してみる

<iframe width="560" height="480" src="https://www.youtube.com/embed/A8dgu6f67-Y" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>

やったぜ。

## 今回わかったこと

こいつが一番遊びやすいや。

![プロコン](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/58380/2e00d9eb-7304-819c-a631-9b6c91c532ae.png "やっぱプロコンだよな")

## おわり

明日はクラスター広報 @MIRINPR さんの「なんか」です！何が書かれるか楽しみ！