LoginSignup
15
6

More than 3 years have passed since last update.

WindowsのUnityからBLEアプリ経由してtoioで遊ぶ

Last updated at Posted at 2020-12-16

toiQ

「ロボットやろうぜ! - toio & Unity 作品動画コンテスト -」で、toiQという作品を作ってみました。

全体像は↓にあるので、よければどうぞ。
https://www.youtube.com/watch?v=eh4HUkaFCJI

作品の経緯

もともとこういうのを作って遊んでいました。
今回のコンテストを知って、これは何かやらねばと思い立ちました。

コンテストに向けて以下のことを意識して何を作るかを考えてました。

  • toioという実物を活かす
  • Unityの得意な映像表現を活かす
  • リアルとバーチャルをつなぐ

toioを使ってバーチャルコースをコントロールするだけでは面白みが足りない気がしたので、以下のような操作手法にしました。

  • toioをチョロQのように引っ張ってリアルボードを走行する
  • toioを手に持ってバーチャルコースの左右を操作
  • toioを引っ張ってちょうどよくブレーキ
  • toioは操作の入力装置として使うだけでなく、自動で動くこともある

最終的に、リアルのtoioが画面に向かって走り、その先にバーチャルなコースが繋がっている という体験を目指すことにしました。

WindowsのBLE

toioはBluetooth LEで制御できます。
toiQは最終的にこういう繋がりになってますが、当初は試行錯誤しました。
toioBleUnity.png

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を追加するのが手っ取り早いかと思います。
vs_cap_1.png
vs_cap_2.png

これでusing Windows.Devices.Bluetooth;などが使えるようになります。

なお、有料ですがUnityからBLEを扱えるようにするAssetもあるようです。
参考記事:toio for Unity Editor Windows BLE対応
このAssetは試してないですが、BLEのサーバーと通信するとのことなので、今回自分が取った手法に近いような気がします。

BLEの中継

中継アプリは今回専用の仕組みは持たず、中継に徹するようにしました。
具体的なtoio操作のBLEコマンドはUnity側で管理してUDPで中継アプリを経由して送るようにしてます。

こういう仕組みにしたのは以下の狙いがあります。

  • 今回以外のtoio連携も見越して中継アプリの汎用性を上げる
  • 製作中は中継アプリを立ち上げっぱなしにすればよく、何かの更新作業の度にUnityも中継アプリも両方起動しなおすようなことを避ける

例:Unityから中継アプリにコマンドを投げる

Unity側からtoioを動かす場合の抜粋
//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で連携する

中継アプリがUnity側からからのUDPを受信したあとの抜粋

//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クラスの中身抜粋
中継アプリで使った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で作成してます。
toiq_dash.png

ゴールした時の紙吹雪や、ゴールエリアの光もVFX Graphで作ってます。
toiq_result.png

3Dスキャンキャラクター

キャラクターには、お気に入りの郷土玩具の犬張子と鳩車を用意。
所有している実物を以前に3Dスキャンしたことがあり、バーチャル空間内にも登場してもらうことにしました。

実物の裏側に小さな磁石を貼っており、toioの磁気センサーを使って読み取ることで、リアルとバーチャルの連動感を強めてます。
inuhariko.jpg
hatoguruma.jpg

まとめ

ありがたいことに、コンテストで大賞頂きました。
「ロボットやろうぜ! - toio & Unity 作品動画コンテスト -」結果発表

toioとUnityの得意なことを両方活かすことを意識して作ってみたことが良い結果につながったのだろうかと思ったりします。
コンテスト関係者のみなさまにも厚く御礼申し上げたい所存です。

誰かに遊んでもらえる機会があれば良いなと思っているので、もしどこかでみかけることがあればよろしくお願いします。

15
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
15
6