toioで射的ゲームの作り方(前編)


# Tsukuba Mini Maker Faireに出られることになった


　[toioで作ってみた！友の会（非公式）](https://www.facebook.com/groups/toiotomo/) の主宰、加藤さんが[TMMF2020](https://tmmf.jp/2020/)に申し込んで当選！、縁あって私も参加させていただくことになりました。
Tokyo Maker Faireは何度か客として参加していましたが、いつか出展してみたいなぁと思っていたところにこのお話！(2020年1月下旬ごろ)、これは飛びつくしかないと。

[後編はこちら](https://qiita.com/kenichih/items/da8985648ba309bad293)

# ネタ出し
　出展できることになったとはいえ、何を出すかは決まっていませんでした。
　持ちネタとしては[toioで作ってみた！友の会（非公式）](https://www.facebook.com/groups/toiotomo/)のLT会 で発表した「[toioでブンドド](https://drive.google.com/open?id=1WtYxdY3I7hE6E7Z5tO8CB8ilkmFBkLVX)」、「[toioを3倍速く回す](https://docs.google.com/presentation/d/1stbIVGFE-C8xWiwiPRONjDoHiZOsoFwbVvSaVIueOg4/)」がありましたが、どうせなら新ネタを作りたい。しかし初めての出展ということもあり、あまり技術的にチャレンジして失敗してもダメというか当日までに完成しなかったらもともこもありません。なので以下のコンセプトで考えることにしました。

* 観に来られる方は親子づれが多いので子供が遊べるようなものにしたい
* 初めての出展なのでトラブルは避けたい　複雑にはしない
* [toio core cube通信仕様v2の新機能(目的地指定移動)](https://toio.github.io/toio-spec/docs/ble_motor)を使ってみたい

　特に新機能の目的値座標指定のモーター制御コマンドは、従来のように常時マットの位置をホスト側で監視しながらモーターの回転制御をする必要がないため、toio core cubeとホスト間のBLE通信量の削減が期待できます。Maker Faireのように2.4GHz帯無線通信が混んでいそうな環境では安定動作に寄与しますし、なによりバッテリーの持ちが良くなります。

　子供が遊ぶゲームっぽいものにしたいのでどういうゲームにするかですが、移動するtoio core cubeを鉄砲のようなもので撃って倒す、というシンプルなものにしようと考えました。toio core cubeの上にレゴでなにか<ruby>的<rp>（</rp><rt>まと</rt><rp>）</rp></ruby>のようなものをつけてそれを撃ってもらう、というものです。

## 機器構成
　機器構成はホスト機としてはBLE内蔵ということもありRaspberry Pi 3B(以下RasPiと省略)を使い、RasPiとノートPCは有線LANで直結し、固定IPアドレスを振っておいて、ノートPC側からtera termなどからsshでログインしてプログラムを起動、終了するものとしました。
　RasPiにスタートボタンのようなものをつけて動かすようにしてもよかったのですが、いろいろトラブルはあるだろうということで、まずは単純にログインして人間がプログラムを動かす形としました。
　また、RasPiには効果音出力用にSpeaker pHat、スコアや状態の表示用にLCD2USBモジュールを取り付けました。

![system.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/431649/3b15a262-a81e-b3a7-f72a-a163fd416e05.png)


# 調査、実装
## 目的値座標指定のモーター制御コマンド
　なにはともあれ、toio core cube ファームウェアver.2で追加された[[目的値座標指定のモーター制御コマンド](https://toio.github.io/toio-spec/docs/ble_motor#%E7%9B%AE%E6%A8%99%E6%8C%87%E5%AE%9A%E4%BB%98%E3%81%8D%E3%83%A2%E3%83%BC%E3%82%BF%E3%83%BC%E5%88%B6%E5%BE%A1)を調べてみます。
仕様書によれば、以下の２種類があります。

* [目的座標を一つ指定モーター制御　タイプ0x03](https://toio.github.io/toio-spec/docs/ble_motor#%E7%9B%AE%E6%A8%99%E6%8C%87%E5%AE%9A%E4%BB%98%E3%81%8D%E3%83%A2%E3%83%BC%E3%82%BF%E3%83%BC%E5%88%B6%E5%BE%A1)
* [目的座標を複数指定モーター制御　タイプ0x04](https://toio.github.io/toio-spec/docs/ble_motor#%E8%A4%87%E6%95%B0%E7%9B%AE%E6%A8%99%E6%8C%87%E5%AE%9A%E4%BB%98%E3%81%8D%E3%83%A2%E3%83%BC%E3%82%BF%E3%83%BC%E5%88%B6%E5%BE%A1)

　今回、的となるtoio core cubeは逃げ回るような動き(動きとしては[ギャラガ](https://ja.wikipedia.org/wiki/%E3%82%AE%E3%83%A3%E3%83%A9%E3%82%AC)、[ギャプラス](https://ja.wikipedia.org/wiki/%E3%82%AE%E3%83%A3%E3%83%97%E3%83%A9%E3%82%B9)のゲーム開始直後に編隊を組むまでの動きをイメージ)をさせたかったので、２つめの「[目的座標を複数指定モーター制御　タイプ0x04](https://toio.github.io/toio-spec/docs/ble_motor#%E8%A4%87%E6%95%B0%E7%9B%AE%E6%A8%99%E6%8C%87%E5%AE%9A%E4%BB%98%E3%81%8D%E3%83%A2%E3%83%BC%E3%82%BF%E3%83%BC%E5%88%B6%E5%BE%A1)」のほうが用途に合っていそうです。複数の目標座標を順に縫って動くように移動するようです。

## 動かしてみる

　「[目的座標を複数指定モーター制御　タイプ0x04](https://toio.github.io/toio-spec/docs/ble_motor#%E8%A4%87%E6%95%B0%E7%9B%AE%E6%A8%99%E6%8C%87%E5%AE%9A%E4%BB%98%E3%81%8D%E3%83%A2%E3%83%BC%E3%82%BF%E3%83%BC%E5%88%B6%E5%BE%A1)」の実際の動きを確かめるために、[toioで作ってみた！友の会（非公式）](https://www.facebook.com/groups/toiotomo/)のLT会のデモで使わせていただいているpythonライブラリ[tomotoio](https://github.com/tomoto/tomotoio/tree/master/tomotoio)をちょい改造してタイプ0x04のモーター制御のパケットを出せるようにします。

```
tomotoio/cube.pyのCubeクラスにこんな感じのメソッド追加
def setMotor4(self, ctrlid: int, goals, writemode:int, timeout: int = 0, movingtype: int = 0, maxspeed: int = 0, speedtype: int =0):
```
　"setMotor4()"というなんのひねりもない名前ですが。まあ、goalsに複数の目的位置(x, y, Θ)を指定して0x04のモーター制御のパケットにエンコードして送信するだけです。
　「[目的座標を複数指定モーター制御　タイプ0x04](https://toio.github.io/toio-spec/docs/ble_motor#%E8%A4%87%E6%95%B0%E7%9B%AE%E6%A8%99%E6%8C%87%E5%AE%9A%E4%BB%98%E3%81%8D%E3%83%A2%E3%83%BC%E3%82%BF%E3%83%BC%E5%88%B6%E5%BE%A1)」によれば、最大29個の目的位置指定が可能ということで、それだけの位置が指定できるなら複雑な軌道で動かすこともできそうです。

## モーション作成
　複数の位置指定が可能ということでその位置に沿ってうごかすための位置の配列をつくる必要があります。とはいえ、複雑な軌道のすべての位置を数学的に算出する、というエレガントな方法ができない(数学さぼってましたもんで...)ので人間が手で位置を指定してゆきます。
　マットの上でtoio core cubeをぐっと押して底面ボタンオンの状態にしたときの座標を読み取り、ホスト側に送信、ホスト側では座標を順番に記録した後にテキストでダンプするスクリプトを書きました。それをつかって動かしたい軌道上の点の座標を何点かとっていきます。
今回、２つのtoio core cubeを同時に動かして逃げ回らせるつもりですが、２つのtoio core cubeがぶつかりそうでぶつからない感じの軌道を手付けで作っていきます。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/431649/c8612180-1f16-d860-dc1b-0256d847ad00.jpeg" width=50% alt="座標を拾う">
動かしたい軌道にあわせて何点か座標を広う。方向転換してから次へ移動、ではなく方向転換しながら移動する指定にすれば軌道は直線的にはならず、ある程度曲線になる。

## 複数目的地指定が動かない
　作った座標の配列を先ほど足したsetMotor4()に入れて動かしてみましたが、動かない。
　なぜだろうと思って調べたところ、どうもRasPi(raspbian)のBLE通信だとMTUが標準サイズは23オクテットで実質ペイロードは20オクテットしかなく、たくさんの目的地座標を入れて長いパケットにしてしまうと送信されないのが原因らしいことがわかりました。システムによってはBLEのMTUサイズを大きくして23オクテットを超える長さで通信できるようですが、RasPiでいろいろ試してみてもさっぱりダメでした。本番まであまり時間もないので、MTUサイズの拡張はあきらめ、20オクテットでどうにかする方法に切り替えます。
　20オクテットの範囲だと目的地座標は２セットぶんしか入りません。幸い、toio core cubeが目的座標に到達したときにnotifyが飛んでくるのと、移動途中でも目的地座標を追加で積むことができるという仕様だったので、

1. 最初は2個ぶん座標を積む
2. 一つ目の座標にtoio core cubeが到達したnotifyがきたら、3個目の座標を一つ追加
3. 2.を繰り返す
![複数目的位置指定.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/431649/ac7dac1e-f0cc-573b-c9d1-7ea7cd698bc9.png)

　という方法で実装しました。これで連続して複数の座標を縫って動く動きを実現できました。
　あとは、先ほど採取した座標の配列を入れて実際にtoio core cubeをうごかしながら、速度値や速度タイプ(だんだんスピードが上がるなど)のパラメータを調整し、それっぽく動くようにします。

## あたり判定と何で撃つか
　的となるtoio core cubeを撃つわけですが、あたり判定としては以下３つがが使えるのではないかと思い試してみました。

* [モーションセンサの衝突検出](https://toio.github.io/toio-spec/docs/ble_sensor#%E8%A1%9D%E7%AA%81%E6%A4%9C%E5%87%BA) で衝撃検出
* [モーションセンサの水平検出](https://toio.github.io/toio-spec/docs/ble_sensor#%E6%B0%B4%E5%B9%B3%E6%A4%9C%E5%87%BA) で一瞬傾いて水平が保てていない状態を検出
* 完全に倒してしまって[モーションセンサの姿勢検出](https://toio.github.io/toio-spec/docs/ble_sensor#%E5%A7%BF%E5%8B%A2%E6%A4%9C%E5%87%BA)で上向き以外を検出する

　そもそも何でtoio core cube(の上の的)を撃つのかというのも決めていなくて、なんとなく[nerf](https://nerf.hasbro.com/ja-jp)、[輪ゴム鉄砲](https://www.amazon.co.jp/gp/product/B07PJPBGDL/)、[空気鉄砲](https://www.kenis.co.jp/onlineshop/product/11130516)のどれかかなとぼんやりと考えていたのですが、展示スペースの図面が開示(2020/1/24)され、見てみると幅180cm、奥行き45cm、のテーブルとのこと。他の方も展示されるのでとても狭い。射撃するための距離はほとんど取れないことがわかりました。
　ほぼ至近距離から撃つことになるので、nerfでは威力が強すぎてヒットするとレゴで作った的がバラバラになるのでダメ。シリンダー式空気鉄砲は思ったより狙うのがむつかしいのと単発なのでとても面倒。結果的に連発式の輪ゴム鉄砲を使うことにしました。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/431649/1efdc38c-3cc0-5636-70c6-86859646affe.jpeg" width=50% alt="輪ゴム鉄砲1"><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/431649/e7118fa1-552a-2921-4672-6e65b2ab14d4.jpeg" width=50% alt="輪ゴム鉄砲2">

　実際に輪ゴム鉄砲で射撃しつつモーションセンサの衝突検出、水平検出、姿勢検出を試してみたところ、輪ゴムが当たった程度ではtoio core cubeは倒れません。なので姿勢検出は不採用。衝突検出と水平検出とで輪ゴムの当たりの振動を判別できるかをテスト。水平検出の角度閾値と衝突検出の閾値をいろいろ調整し、結局は衝突検出のみでいくことにしました。
 ただし、この閾値はtoio core cubeの上に載せたものの重さによって変わることが分かったので、的にあわせた値をセットしておく必要がありました。
 あと、この方法だとtoio core cubeどうしが衝突した場合でも当たり判定になってしまうという問題はあるのですが、それぞれがぶつからないように軌道制御するのは難しいため、今回これは目をつぶることにしました。

## 的と演出
　LEGOでなんか的を作るつもりでしたが、あまり大きくしてもダメなので、思案の結果、[LEGO Minecraft](https://www.amazon.co.jp/%E3%83%AC%E3%82%B4-LEGO-30393-%E3%83%9E%E3%82%A4%E3%83%B3%E3%82%AF%E3%83%A9%E3%83%95%E3%83%88-%E3%82%B9%E3%83%86%E3%82%A3%E3%83%BC%E3%83%96%E3%81%A8%E3%82%AF%E3%83%AA%E3%83%83%E3%83%91%E3%83%BC/dp/B07NDXJNPH/ref=pd_aw_sbs_21_1/356-7905405-7675543?_encoding=UTF8&pd_rd_i=B07NDXJNPH&pd_rd_r=5c498ac2-34c2-44c7-b449-bcedce043739&pd_rd_w=YzcCl&pd_rd_wg=vKA6X&pf_rd_p=bff3a3a6-0f6e-4187-bd60-25e75d4c1c8f&pf_rd_r=BSHJKFDE63WHGZ46HVZM&psc=1&refRID=BSHJKFDE63WHGZ46HVZM])を採用しました。
　逃げ回るクリーパーを倒せ、というストーリー性もつけられますし、小学生にも人気だろうという目論見です。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/431649/43b085df-4dc6-86da-d576-74db75dcf4f5.jpeg" width=50% alt="LEGO MINECRAFTの的">

　的が決まったのでその重さに合わせた衝撃検出の閾値の調整と座標配列による軌道にそって動かしつつ、速度は加速したり減速したりするように調整します。あまり急激にストップしたり方向転換したりするとそれも衝撃扱いになり「当たり」になってしまうのでほどほどの速度に調整します。
　また、「当たり」のときは数秒間くるくると回って当たったことを表現。この間に再ヒットしても「当たり」扱いにならないようにするなどアドホックな対応をいれました。

　「当たり」の際は効果音を鳴らしたくなったので、ちょうど手元にPimoroniの[Speaker pHat](https://www.switch-science.com/catalog/3219/)があったのでRasPiに組み込んで鳴らすようにしました。
　音源はフリー素材サイト「[効果音ラボ](https://soundeffect-lab.info/sound/anime/)」から選んで「ぼよよーん」という効果音を選択。
　pythonでの再生はpygameを使用しました。(「[Pygame で音を読み込み、再生する](https://qiita.com/sf_/items/b6f59128ff44c1eca0e9)」)

## スコア表示
　「当たり」になったらスコア加算してスコアをどこかに表示したくなってきましたが、普通にHDMIディスプレイを繋ぐのも普通すぎてつまらないなぁと思い、Tokyo Maker Faireでゲットした[PIXOO](http://wisetech.co.jp/brand/DIVOOM/product/pixoo/index.html)表示しようかと挑戦しましたが、なかなかRasPiのbluetoothでのコントロールまで達せず断念。
　たまたま[LCD2USB](https://github.com/harbaum/LCD2USB)仕様の文字表示LCDモジュールが手元にあったので代わりにそれに表示するようにしました。まあ、本番ではプレイヤーはスコアなんかさっぱり見てなかったですが、スクリプトの起動/終了を行ったり状態を見ているこちらとしては便利でした。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/431649/87a712cd-be15-f989-9343-848533f41877.jpeg" width=50% alt="Speaker pHatとLCD2USB">

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/431649/433dd04a-157a-a320-66ae-707a62953878.png" width=50%> [USB port 1602 LCD Module for Pi (LCD2USB)](https://www.friendlyarm.com/index.php?route=product/product&path=81&product_id=136)
LCD2USB用Pythonライブラリ https://pypi.org/project/lcd2usb/

## ゲーム内容

### かんたんモードの追加

　動き回る的を狙って当てる、というのは的が近いとはいえ低年齢の子には難しいかなと思い、かんたんモードも作ることにしました。的は静止した状態で、当たると「当たり」表現をしたあとに、別の場所へすーっと移動して止まる、というものです。

[![簡単モード　的は止まっていて当たると別の位置に移動](http://img.youtube.com/vi/OX2hYu_99j0/0.jpg)](http://www.youtube.com/watch?v=OX2hYu_99j0)

 何回か的に当てると終了。

### ちょっとムズいモード

　ゲームスタートと同時に的は動き回ります。決まった軌道にそって移動しているのですが、ぱっと見、ランダムに逃げているような感じの起動になっています。
　輪ゴムが当たるとポイント加算、所定の移動パターンを全部走行しきると終了。

[![ちょっとムズいモード　的は動き回る](http://img.youtube.com/vi/Su-xKABGDyg/0.jpg)](http://www.youtube.com/watch?v=Su-xKABGDyg)

　２つのtoio core cubeそれぞれの軌道の位置と速度を調整してお互いが衝突しないようにはしたつもりですが、片方がヒット状態で止まっていると走行タイミングがずれてやっぱりどこかで衝突は発生することがありますが、気にしないことにした。

　それぞれのゲームは一回ごとにスクリプトを起動しなおします。客の年齢をみながらかんたん、かちょっとムズい、かを選んで起動します。

#なんとか形になりました

いろいろ調整して本番前日の2/14になんとか動くようになりました。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/431649/c75a9a6e-4c64-d18b-39d7-9ad581a7131d.png" alt="kouji_tokkankouji.png" width=50%>


[後編](https://qiita.com/kenichih/items/da8985648ba309bad293)に続く