#toiQ
「ロボットやろうぜ! - toio & Unity 作品動画コンテスト -」で、toiQという作品を作ってみました。
toio & Unityの「ロボットやろうぜ!」で大賞頂いた作品のダイジェスト動画作ってみました。
— foka (@foka22ok) December 8, 2020
まだ家族しか遊んでないので、どこかで人に見てもらえる機会があれば...。
作品の全体は↓こちらですhttps://t.co/LPyVq0dUQD#toio #Unity #ロボやろ pic.twitter.com/ZSCQHe1G3Q
全体像は↓にあるので、よければどうぞ。
https://www.youtube.com/watch?v=eh4HUkaFCJI
#作品の経緯
もともとこういうのを作って遊んでいました。
今回のコンテストを知って、これは何かやらねばと思い立ちました。
toioに犬張子(本物)を載せて、Unity内の犬張子(3D Scan)の位置と連動テスト。
— foka (@foka22ok) June 17, 2020
PCキーボードでtoioをBLE経由で動かしたり、toioの開発者向けマット上の位置情報取得するのをとりあえず試してみました。
なお、WinのUnityで直接BLE操作するのは壁が多すぎて断念。#toio #おうちでロボット開発 pic.twitter.com/LHBKo5TeoV
コンテストに向けて以下のことを意識して何を作るかを考えてました。
- toioという実物を活かす
- Unityの得意な映像表現を活かす
- リアルとバーチャルをつなぐ
toioを使ってバーチャルコースをコントロールするだけでは面白みが足りない気がしたので、以下のような操作手法にしました。
- toioをチョロQのように引っ張ってリアルボードを走行する
- toioを手に持ってバーチャルコースの左右を操作
- toioを引っ張ってちょうどよくブレーキ
- toioは操作の入力装置として使うだけでなく、自動で動くこともある
最終的に、リアルのtoioが画面に向かって走り、その先にバーチャルなコースが繋がっている という体験を目指すことにしました。
#WindowsのBLE
toioはBluetooth LEで制御できます。
toiQは最終的にこういう繋がりになってますが、当初は試行錯誤しました。
##toio SDK for Unity(見送り)
コンテストのきっかけとなったtoioとUnityをつなぐSDKです。
ただ、残念ながらWindowsではシミュレータまでで、実物toioには連動しないので見送りました。
https://morikatron.com/t4u/
##toio.js(見送り)
公式に toio.js というjavascript SDKがあります。
ただ、Windowsの場合は、導入に手がかかりそうなのとBluetooth 4.0 USB adapter、USBのBLEドングルが別途に必要になるらしく、WindowsノートPC搭載のBLEをそのまま使いたかったのでtoio.jsの導入は見送りました。
→参考記事:toioのjavascriptライブラリが公開されたのでexample動かしてみた
##Unityから直接WindowsのBLEを扱う(断念)
当初採用予定だった方法です。
UnityはC#で書けるし、C#でWindowsのBLEを扱っている前例も普通にみつかるし、いけるだろ、と思ったらダメでした。
Unity Editor、スタンドアローンモード書き出しはBLEなどの機能が含むWinRTと呼ばれるWindows APIにアクセスできないようでした。
スタンドアローンではなく、UWP用のビルドをすることでWinRTが扱えそうですが、自分の環境ではスムーズにうまくいかなかったのと、BLEが使いたいだけでそれ以外はUWPである必要が無いので、Unityから直接BLEを扱うのは断念。
##C#のWindowsコンソールアプリを作って中継する(採用)
別途、中継用のアプリを用意することにしました。
タスクトレイ常駐アプリの方が目立たなくて良いと思いますが、今回はログを見ながら進めたかったのでコンソールアプリにしました。
ただ、WindowsのBLEはWinRTの方に(なぜか)属していて、UWPアプリだとBLEが普通に使えますが、通常はコンソールアプリではWinRTのBLE機能は使えません。
でも、UWPでなくてもWinRTを使えるようにする方法がいくつかあります。
参考記事:WPFアプリ(.Net Framework)でUWPのAPIを使う
VisualStudioの NuGetパッケージの管理
からMicrosoft.Windows.SDK.Contracts
を追加するのが手っ取り早いかと思います。
これでusing Windows.Devices.Bluetooth;
などが使えるようになります。
なお、有料ですがUnityからBLEを扱えるようにするAssetもあるようです。
参考記事:toio for Unity Editor Windows BLE対応
このAssetは試してないですが、BLEのサーバーと通信するとのことなので、今回自分が取った手法に近いような気がします。
#BLEの中継
中継アプリは今回専用の仕組みは持たず、中継に徹するようにしました。
具体的なtoio操作のBLEコマンドはUnity側で管理してUDPで中継アプリを経由して送るようにしてます。
こういう仕組みにしたのは以下の狙いがあります。
- 今回以外のtoio連携も見越して中継アプリの汎用性を上げる
- 製作中は中継アプリを立ち上げっぱなしにすればよく、何かの更新作業の度にUnityも中継アプリも両方起動しなおすようなことを避ける
###例:Unityから中継アプリにコマンドを投げる
//Move(30,30)という感じでどこかから呼び出す
//toio移動用の関数
void Move(int leftMotor, int rightMotor) {
//左モーターの回転方向
byte leftMotorVector = 0x01;
if (leftMotor < 0) {
leftMotorVector = 0x02;
}
//右モーターの回転方向
byte rightMotorVector = 0x01;
if (rightMotor < 0) {
rightMotorVector = 0x02;
}
//toioを移動させるBLEコマンド
byte[] dataValue = { 0x01, 0x01, leftMotorVector, Convert.ToByte(Mathf.Abs(leftMotor)), 0x02, rightMotorVector, Convert.ToByte(Mathf.Abs(rightMotor)) };
//中継アプリで処理を分岐するための追加コマンドを足す
byte[] result = GetSendTargetData(0,(int)ToioActionType.MOVE,dataValue);
//中継アプリにUDP送信する。SendUDPの中身は省略
SendUDP(result);
}
//中継アプリ用の追加コマンドを取得する。deviceNoはtoioを複数台使う時用
byte[] GetSendTargetData(int deviceNo, int actionNo, byte[] dataValue) {
byte[] baseValue = { Convert.ToByte(deviceNo), Convert.ToByte(actionNo) };
return Enumerable.Concat(baseValue, dataValue).ToArray();
}
//中継アプリがBLEのどのCharacteristicsを使うかを振り分けるためのenum
public enum ToioActionType
{
SETTING,
MOVE,
SOUND,
LIGHT
}
###例:中継アプリがUnityからコマンドを受け取ってtoioとBLEで連携する
//toioデバイス管理用の独自Class。BluetoothLEDeviceをラップしている。具体的な中身は下部参照
//今回はtoio1個だけなのでToioDeviceも1個。
ToioDevice toio;
//別の個所でnew したUdpClientを別ThreadからOnUdpRecieveを呼んでデータ受信
void OnUdpRecieve() {
while (isActive) {
IPEndPoint remoteEP = new IPEndPoint(IPAddress.Any, 10001);
byte[] data = udpClient.Receive(ref remoteEP);
SendDataToToio(data);
}
}
//UnityからUDP経由で受け取ったデータを読み取って処理分岐
void SendDataToToio(byte[] data) {
//Unityからの追加コマンドその1。toio番号を読み取る
byte toioNo = data[0];
//Unityからの追加コマンドその2。toioのアクション番号を読み取る。どのCharacteristicsを使うか判別する時に使う
byte actionNo = data[1];
//Characteristicsに渡すコマンドだけを切り取る
int dataLength = data.Length - 2;
byte[] buffer = new byte[dataLength];
Array.Copy(data, 2, buffer, 0, dataLength);
//ToioDeviceにBLEコマンドを送る。ToioActionTypeはUnityの同enumと同じ中身
switch (actionNo) {
case (int)ToioActionType.SETTING:
toio.SetDeviceSetting(buffer);
break;
case (int)ToioActionType.MOVE:
toio.SetMove(buffer);
break;
case (int)ToioActionType.SOUND:
toio.SetSound(buffer);
break;
case (int)ToioActionType.LIGHT:
toio.SetLight(buffer);
break;
}
}
中継アプリで作ったToioDeviceクラスの中身も一部載せておきます。
↓クリックで開きます。
ToioDeviceクラスの中身抜粋
using System;
using System.Linq;
using System.Threading.Tasks;
using System.Runtime.InteropServices.WindowsRuntime;
using Windows.Devices.Bluetooth;
using Windows.Devices.Bluetooth.GenericAttributeProfile;
using Windows.Storage.Streams;
class ToioDevice
{
//toio本体のUUID
static readonly public string uuid = "10B20100-5B3B-4571-9508-CF3EFCD7BBAE";
//各機能のCharacteristic UUID
readonly string light_characteristicUUID = "10B20103-5B3B-4571-9508-CF3EFCD7BBAE";
readonly string motor_characteristicUUID = "10B20102-5B3B-4571-9508-CF3EFCD7BBAE";
readonly string scan_characteristicUUID = "10B20101-5B3B-4571-9508-CF3EFCD7BBAE";
readonly string motion_characteristicUUID = "10B20106-5B3B-4571-9508-CF3EFCD7BBAE";
readonly string setting_characteristicUUID = "10B201FF-5B3B-4571-9508-CF3EFCD7BBAE";
readonly string sound_characteristicUUID = "10B20104-5B3B-4571-9508-CF3EFCD7BBAE";
readonly string magnetic_characteristicUUID = "10B20106-5B3B-4571-9508-CF3EFCD7BBAE";
readonly string functionalBtn_characteristicUUID = "10B20107-5B3B-4571-9508-CF3EFCD7BBAE";
//BLEDevice
BluetoothLEDevice device;
//各機能のCharacteristic
GattCharacteristic lightCharacteristic;
GattCharacteristic motorCharacteristic;
GattCharacteristic scanCharacteristic;
GattCharacteristic motionCharacteristic;
GattCharacteristic settingCharacteristic;
GattCharacteristic soundCharacteristic;
GattCharacteristic magneticCharacteristic;
GattCharacteristic functionalBtnCharacteristic;
//データ読み取り用のdelegate
public delegate void ValueChangeCallback(byte[] data);
//データ読み取り時のcallback
public System.Action OnDeviceReadyCallback;
public ValueChangeCallback OnScanValueChangeCallback;
public ValueChangeCallback OnMotionValueChangeCallback;
public ValueChangeCallback OnMagneticValueChangeCallback;
public ValueChangeCallback OnFunctionalBtnValueChangeCallback;
//外のClassからBluetoothLEDeviceを渡してnew ToioDevice()します
public ToioDevice(BluetoothLEDevice d) {
device = d;
device.ConnectionStatusChanged += OnConnectionStatusChanged;
}
//toioに接続
public async void Connect() {
//toioのBLEのserviceを取得
GattDeviceServicesResult serviceResult = await device.GetGattServicesForUuidAsync(new Guid(uuid));
//toioのそれぞれのCharacteristicを取得
lightCharacteristic = await GetCharacteristic(serviceResult, light_characteristicUUID);
motorCharacteristic = await GetCharacteristic(serviceResult, motor_characteristicUUID);
scanCharacteristic = await GetCharacteristic(serviceResult, scan_characteristicUUID);
motionCharacteristic = await GetCharacteristic(serviceResult, motion_characteristicUUID);
settingCharacteristic = await GetCharacteristic(serviceResult, setting_characteristicUUID);
soundCharacteristic = await GetCharacteristic(serviceResult,sound_characteristicUUID);
magneticCharacteristic = await GetCharacteristic(serviceResult, magnetic_characteristicUUID);
functionalBtnCharacteristic = await GetCharacteristic(serviceResult, functionalBtn_characteristicUUID);
//toioからBLEのnotifyを受け取るための準備
GattCommunicationStatus scanStatus = await scanCharacteristic.WriteClientCharacteristicConfigurationDescriptorAsync(GattClientCharacteristicConfigurationDescriptorValue.Notify);
if (scanStatus == GattCommunicationStatus.Success) {
scanCharacteristic.ValueChanged += OnScanCharacteristic_ValueChanged;
}
GattCommunicationStatus motionStatus = await motionCharacteristic.WriteClientCharacteristicConfigurationDescriptorAsync(GattClientCharacteristicConfigurationDescriptorValue.Notify);
if (motionStatus == GattCommunicationStatus.Success) {
motionCharacteristic.ValueChanged += OnMotionCharacteristic_ValueChanged;
}
GattCommunicationStatus magneticStatus = await magneticCharacteristic.WriteClientCharacteristicConfigurationDescriptorAsync(GattClientCharacteristicConfigurationDescriptorValue.Notify);
if (magneticStatus == GattCommunicationStatus.Success)
{
magneticCharacteristic.ValueChanged += OnMagnetic_ValueChanged;
}
GattCommunicationStatus functionalBtnStatus = await functionalBtnCharacteristic.WriteClientCharacteristicConfigurationDescriptorAsync(GattClientCharacteristicConfigurationDescriptorValue.Notify);
if (functionalBtnStatus == GattCommunicationStatus.Success)
{
functionalBtnCharacteristic.ValueChanged += OnFunctionalBtn_ValueChanged;
}
OnDeviceReadyCallback();
}
//BLEの各Characteristicを取得する処理を関数にして使い回す
async Task<GattCharacteristic> GetCharacteristic(GattDeviceServicesResult serviceResult,string uuidStr)
{
GattCharacteristicsResult characteristics = await serviceResult.Services.First().GetCharacteristicsForUuidAsync(new Guid(uuidStr));
return characteristics.Characteristics.First();
}
//移動用のCharacteristicにBLEコマンドを渡す
public async void SetMove(byte[] value) {
await motorCharacteristic.WriteValueAsync(value.AsBuffer());
}
//ライト操作用のCharacteristicにBLEコマンドを渡す
public async void SetLight(byte[] value)
{
await lightCharacteristic.WriteValueAsync(value.AsBuffer());
}
//読み取りセンサーのnotifyを受け取ったら外のclassにcallbackする
private void OnScanCharacteristic_ValueChanged(GattCharacteristic sender, GattValueChangedEventArgs args) {
IBuffer buffer = args.CharacteristicValue;
byte[] readBytes = new byte[buffer.Length];
using (DataReader reader = DataReader.FromBuffer(buffer)) {
reader.ReadBytes(readBytes);
OnScanValueChangeCallback(readBytes);
}
}
//一部関数省略
}
中継アプリもUnityに合わせてC#にしたことで、上記例のenum ToioActionType
のようにどちらも同じ書き方を使い回せます。
コピペで済むし、あっちこっちでこの処理番号はなんだったかな、みたいなことにならなくて良いので便利です。
詳細は省略しますが、toioからの読み取りデータも中継アプリからUnity側にbyte[]をまるっと渡して、Unity側で処理してます。
#遊び方を作る
上記のような感じで、中継アプリをはさんでtoioとUnityを連動できるようにしました。
具体的に使った機能は以下のようになります。
- ひっぱってパワーを貯める
- 読み取りセンサーのポジション、回転情報
- トントンと叩いて走り始める
- モーションセンサーのダブルタップ検出
モーター制御の自動走行
- バーチャルコースの走行コントロール
- 読み取りセンサーのtoio回転情報
- バーチャルコースのブレーキ
- 読み取りセンサーのポジション
- 犬張子と鳩車のキャラクターチェンジ
- 磁気センサーのSN極判定
- toioの状態表示ランプ
- コース紹介時はランプを赤
操作可能時はランプを青
ゴール時はランプが7色に光る
- 最終結果のtoioダンス
- モーター制御の自動走行
#Unityのバーチャルコース
Unity側はさほど特殊なことはしてないですが、楽しそうに見える、HDRP・VFX Graphを使ってみる、3Dスキャンデータを走らせる、ということを意識して作りました。
HDRPでコースシーンを作成し、PostProcessingで色味等の見た目調整をしています。
この画面の集中線や、ブレーキの火花はVFX Graphで作成してます。
ゴールした時の紙吹雪や、ゴールエリアの光もVFX Graphで作ってます。
##3Dスキャンキャラクター
キャラクターには、お気に入りの郷土玩具の犬張子と鳩車を用意。
所有している実物を以前に3Dスキャンしたことがあり、バーチャル空間内にも登場してもらうことにしました。
実物の裏側に小さな磁石を貼っており、toioの磁気センサーを使って読み取ることで、リアルとバーチャルの連動感を強めてます。
#まとめ
ありがたいことに、コンテストで大賞頂きました。
「ロボットやろうぜ! - toio & Unity 作品動画コンテスト -」結果発表
toioとUnityの得意なことを両方活かすことを意識して作ってみたことが良い結果につながったのだろうかと思ったりします。
コンテスト関係者のみなさまにも厚く御礼申し上げたい所存です。
誰かに遊んでもらえる機会があれば良いなと思っているので、もしどこかでみかけることがあればよろしくお願いします。