toioをGamepadで操作する簡単ツールを作った話 

#サマリ
結論から申し上げますと、toio™️のキューブをgamepadを使ってラジコン操作出来る、マルチプラットフォームツールを作りましたよと言う話です。動画でまとめるとこんな感じの機能があります。
<blockquote class="twitter-tweet"><p lang="ja" dir="ltr"><a href="https://twitter.com/hashtag/toio?src=hash&amp;ref_src=twsrc%5Etfw">#toio</a> のキューブをブラウザから各種Gamepad(DUALSHOCK 4やJoy-Con)を使ってラジコン操作出来る、マルチプラットフォームツールを作りました。<br>詳しくは下記Qiita記事にて。<a href="https://t.co/2g1W8Jd5RK">https://t.co/2g1W8Jd5RK</a><br><br>ツール↓<a href="https://t.co/D4bpq19sAb">https://t.co/D4bpq19sAb</a><a href="https://twitter.com/hashtag/WebBluetooth?src=hash&amp;ref_src=twsrc%5Etfw">#WebBluetooth</a> <a href="https://twitter.com/hashtag/GamepadAPI?src=hash&amp;ref_src=twsrc%5Etfw">#GamepadAPI</a> <a href="https://t.co/liPmep72ft">pic.twitter.com/liPmep72ft</a></p>&mdash; Tetsunori NAKAYAMA | 中山 哲法 (@tetunori_lego) <a href="https://twitter.com/tetunori_lego/status/1223506804245532672?ref_src=twsrc%5Etfw">February 1, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>YouTube -> https://youtu.be/Adt6j601e4E

#成果物
[こちら](https://tetunori.github.io/toioCoreCubeGampadControl/)にアクセスするだけで使えます。[ソースコード](https://github.com/tetunori/toioCoreCubeGampadControl)もOSSでどうぞ。
<img width="800" alt="screen_10.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/542654/6fddd51a-34f9-4814-23c4-1a24dc65e594.png">

# 主な特徴
 - Windows、Mac対応。Androidはサポートしているはずだが、未確認。(iOS、iPadOSは非対応）
 - Dualshock4、Joy-Conを始めとする無線コントローラーやUSBゲームパッド(一部機能限定)に対応
 - 複数のラジコン操作方法に対応。２つのキューブを１つのコントローラーから制御する事も可能。
   - Joy-Conのセパレートスタイルにも対応。2人でラジコンレースできるよ！
<img width="500" alt="IMG_0718.JPG" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/542654/64b974c5-bb67-b6d6-dfce-450dd8746c12.jpeg">

#きっかけ
[先日の記事](https://qiita.com/tetunori_lego/items/0670587c06fa9e9faf52)の内容を開発している時、gamepadAPIでJoy-Conなど簡単に使えて大変気持ちが良かったので、単純にラジコン操作出来るツールが欲しくなったまでです。
コントローラーが無線になるだけで、自由度は爆上がりですし。

# 準備
## システム構成
<img width="800" alt="topology.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/542654/025593ce-15e6-b3b1-6196-ce0db6c1c742.png">

## Hardware
1. キューブの電源を入れましょう。
2. Dualshock 4やJoy-Con(L&R)などのBluetooth/USB gamepadを用意します。特にBluetooth系のコントローラーは事前にPCとペアリング作業を終えてください。<BR><img width="500" alt="gamepads.jpg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/542654/36d134e2-132b-7647-384a-ce9015b157af.jpeg">

## Software
1. 冒頭でも紹介した[このツール](https://tetunori.github.io/toioCoreCubeGampadControl/)にアクセスしてください。Google Chromeを強く推奨します。
2. GamepadのPS/Homeボタンを押して、画面上のgamepadアイコンが図の様に白く変われば認識できています。(Homeボタン系がないUSBコントローラー等はSELECT/START同時押しで代用です。)<BR><img width="300" alt="105.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/542654/05d27717-aded-0511-5aab-a8567fcfa283.png">
3. それと同時にキューブを接続するためのウィンドウが立ち上がりますので、キューブを選択して接続してください。ダイアログ閉じちゃった場合などは手動で"Connect Cube 1/2"ボタンを押して一つずつ接続してください。
4. 全ての準備が上手くいけば、下記の様な画面に遷移していると思います。これで準備は完了です！<BR><img width="300" alt="110.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/542654/7eba4c68-dcda-4f19-56f6-d57df98f004b.png">

# 各種モードについて
シンプルツールですとか言っておきながら、色んな操作モード作って複雑にしてしまったので図にしました。<BR><img width="719" alt="mode_description.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/542654/d68bbf5e-d177-a963-5f82-8bd6d1f7073b.png">
少しテキストでフォローすると、ざっくり以下の使い分けになっています。

- Single Cube Control（１つのgamepad で１つのキューブを操作）
  - Normal mode
     - <img width="400" alt="IMG_0716.JPG" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/542654/2421a53c-0f83-fb60-8270-1038ec84a853.jpeg">
      - Left analog stick: Move
      - Right analog stick(X-Axis): Rotation(その場回転)
  - Stick mode
      - Left analog stick(Y-Axis): Speed
      - Right analog stick(X-Axis): Direction
- Double Cube Control（１つのgamepad で２つのキューブを操作）
  - Combined mode
     - <img width="400" alt="IMG_0719.JPG" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/542654/87e705ae-5da5-38de-4c8b-7d41ee282e06.jpeg">
     - Left analog stick: Cube1 Move
     - Right analog stick: Cube2 Move
  - Separated mode
     - Mainly, for Joy-Con L/R<BR><img width="400" alt="IMG_0718.JPG" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/542654/0e68c2ec-6191-a7c9-3b70-663816878707.jpeg">
     - Left analog stick(rotate 90 deg.): Cube1 Move
     - Right analog stick(rotate -90 deg.): Cube2 Move

# 画面仕様
## Single Cube Control
<img width="800" alt="301.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/542654/d97ec26a-9595-4961-95dc-1e986a4071e2.png">
## Double Cube Control
<img width="800" alt="302.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/542654/b7264b9e-930d-9100-502e-57296fadd8d8.png">

# 操作方法
## 全てに共通
|カテゴリ|Gamepad操作<BR>DUALSHOCK 4/Joy-Con|UI例|
|---|---|---|
|Cube1/2の切り替え|OPTION/‘+’ button|Cube1は水色に、Cube2は緑色にそれぞれ光ります|
|設定リセット|PS/HOME button|USB gamepadの場合はSTART+SELECT同時押しで代用|
|スピード調整|L1/R1, L/R button|デフォルト値は60|

## Single Cube Control
### モード共通

|カテゴリ|Gamepad操作<BR>DUALSHOCK 4/Joy-Con|UI例|
|---|---|---|
|gamepadの登録|PS/Home button|未登録な場合|
|gamepad1/2の切り替え|PS/Home button|登録済みの場合|
|操作モードの切り替え|SHARE/‘-‘ button|Normal<->Stickをトグルします|
|Double Cube Controlへの切り替え|PS/Home button長押し| - |

### Normal mode

|カテゴリ|Gamepad操作<BR>DUALSHOCK 4/Joy-Con|UI例|
|---|---|---|
|アナログ操作|Left analog stick|<img width="300" alt="IMG_0716.JPG" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/542654/02418fdb-af88-36a4-3fe1-63c16a684b5d.jpeg">|
|前進/後退|UP/DOWN button|最速でまっすぐ前進/後退します|
|その場回転|X-axis of right analog stick or LEFT/RIGHT button|CIRCLE/A, SQUARE/Y buttonも最速回転として使えます|
Working demo movie(will update...)

### Stick mode
|カテゴリ|Gamepad操作<BR>DUALSHOCK 4/Joy-Con|UI例|
|---|---|---|
|スピードコントロール|Y-axis of left analog stick or left/right trigger|UP/DOWN buttonも最速設定として使用できます|
|方向コントロール|X-axis of right analog stick|CIRCLE/A, SQUARE/Y buttonも最大値として使えます|
Working demo movie(will update...)

## Double Cube Control
### モード共通
|カテゴリ|Gamepad操作<BR>DUALSHOCK 4/Joy-Con|UI例|
|---|---|---|
|操作モードの切り替え|SHARE/- button|Combined<->Separatedとトグルします|
|Single Cube Controlへ切り替え|PS/Home button|戻る時は単押しでオッケーです|

### Combined mode
|カテゴリ|Gamepad操作<BR>DUALSHOCK 4/Joy-Con|UI例|
|---|---|---|
|Cube1 アナログ操作|Left analog stick|<img width="300" alt="IMG_0719.JPG" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/542654/483e638e-fa4d-e70e-0ee6-91ec418abfd1.jpeg">|
|Cube1 前進/後退|UP/DOWN button|最速で前進/後退|
|Cube1 回転|LEFT/RIGHT button|最速で進みながら回転|
|Cube1 その場回転|L2/ZL triggerを押しながらLEFT/RIGHT button|最速でその場回転|
|Cube2 アナログ操作|Right analog stick|Cube1の方の図を参照|
|Cube2 前進/後退|TRIANGLE/CROSS button|最速で前進/後退|
|Cube2 回転|SQUARE/CIRCLE button|最速で進みながら回転|
|Cube2 その場回転|R2/ZR triggerを押しながらSQUARE/CIRCLE button|最速でその場回転|
Working demo movie(will update...)

### Separated mode
|カテゴリ|Gamepad操作<BR>DUALSHOCK 4/Joy-Con|UI例|
|---|---|---|
|Cube1 アナログ操作|Left analog stick<BR>(90度回転)|<img width="300" alt="IMG_0723.JPG" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/542654/44b612ba-9758-ced5-1e28-93a5ce00ac77.jpeg">|
|Cube1 前進/後退|LEFT/RIGHT button|最速で前進/後退|
|Cube1 回転|UP/DOWN button|最速で進みながら回転|
|Cube2 Analog move|Right analog stick<BR>(-90度回転)|<img width="300" alt="IMG_0722.JPG" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/542654/8d57e845-8657-bad1-fa44-4fb8fac9cf63.jpeg">|
|Cube2 前進/後退|SQUARE/CIRCLE button|最速で前進/後退|
|Cube2 回転|TRIANGLE/CROSS button|最速で進みながら回転|
Working demo movie(will update...)

# SWのポイント
## gamepadを振動させるには
今回gamepad1/2に登録・切り替えする際にgamepadを振動させています。Daulshock 4やJoy-Conで体験できます。
振動デバイスが異なる両者ですが、どちらも同一ソースコードで動きます。

```Javascript:振動させるコード
const _vibrateGamePad = ( gamePad, duration ) => {
    if ( gamePad.vibrationActuator ) {
        gamePad.vibrationActuator.playEffect( "dual-rumble", { 
            duration: duration, 
            weakMagnitude: 1.0,
            strongMagnitude: 1.0 
        } );
    }
}
```
それぞれ良いところがありますがJoy-Conのhapticはキレが良くて好きですね。
※Android Chromeでは振動非サポートの様。

## gamepadのdescriptionを取得する
gamepadが接続された時に機種名を表示していますが、Daulshock 4(1st/2nd gen)やJoy-Conが接続された時のみ特別な表記にしています。特に理由はありませんが。。。
取得や判別は容易で`gamepad.id`にアクセスするだけです。

```Javascript
const getDescription = ( index ) => {
    let description;
    const gamepad = navigator.getGamepads()[ /* gamepad index */ ];
    if( gamepad ){
        if( isJoyCon( index ) ){
            description = 'Joy-Con L+R';
        }else if( isDualShock4_1stGen( index ) ){
            description = 'DUALSHOCK 4(1st Gen)';
        }else if( isDualShock4_2ndGen( index ) ){
            description = 'DUALSHOCK 4(2nd Gen)';
        }else{
            description = gamepad.id;
        }
    }
    return description;
}

const isDualShock4_2ndGen = ( gamepadIdx ) => {
    const gamepad = navigator.getGamepads()[ /* gamepad index */ ];
    if( gamepad ){
        const gamepadDesc = gamepad.id;
        if( gamepadDesc.indexOf('Wireless Controller (STANDARD GAMEPAD Vendor: 054c Product: 09cc)') !== -1 ){
            return true;
        }else{
            return false;
        }
    }else{
        return false;
    }
}
```

## USBコントローラの注意点
市販されているUSBコントローラーはボタンの数が少ないものもあるため、Button 10以降は無いものが多かったです。これらは無くても動作するように設計すべきでした。今回は実装した後に気が付いてしまったので、work aroundで乗り切りました。

```Javascript:HOMEボタンが存在しない場合をケアした例
const gamePad = navigator.getGamepads()[ idGamepad ];
    const GAMEPAD_BT_HOME = 16;
    const GAMEPAD_BT_8 = 8;
    const GAMEPAD_BT_9 = 9;
    let currentHomeButtonStatus;
    if( gamePad.buttons[ GAMEPAD_BT_HOME ] ){
        currentHomeButtonStatus = gamePad.buttons[ GAMEPAD_BT_HOME ].value;
    }else{
        currentHomeButtonStatus = gamePad.buttons[ GAMEPAD_BT_8 ].value && 
                                    gamePad.buttons[ GAMEPAD_BT_9 ].value;
    }
```

# 動作確認済機器
## PC
 - OK: Windows10 Home / vaio VJP132
 - 未: Mac、、、はどなたか確認してもらえないかなぁ。
 - NG: Android 8.0.0 / SOV34
   - OS側に取られているのか、Chrome側にはgamepad制御権回ってこなかった？
 - OK: Android 6.0.1 / vaio  phone VPA051

## gamepad
 - OK: [Bluetooth] Dualshock 4 (1st gen.)
 - OK: [Bluetooth] Dualshock 4 (2nd gen.)
 - OK: [Bluetooth] Joy-Con
 - OK: [USB] iBUFFALO BSGP801
 - OK: [USB] 『プレイステーション クラシック』同梱USBコントローラー

# 所感と考察
- gamepad APIは簡単に扱えるのは良いが、思ったよりgamepad機種(ボタン配置)差分があって、その辺をケアするのは大変ですね
- Androidでもう少しすんなり使えるかなーと思いましたが、スマホのUI操作の方に優先権があるのは致し方ないか。選択権があると良いのにな。
- いろんなプロトタイプに使ってもらえると幸いです。