ゆべねこの足跡

アプリケーションの個人開発をはじめとした話を書いていきます

ラズパイ x Arduino x Unity x HMDで簡易的なテレイグジスタンス体験をする

今回は夏休み中に趣味兼研究目的で作っていた簡易的なテレイグジスタンス体験ができる装置の紹介をしたいと思います。

目次

テレイグジスタンスとは?

Wikipediaによると以下のように書かれています。

テレイグジスタンス(英: Telexistence、遠隔臨場感、遠隔存在感)とは、バーチャルリアリティの一分野であり、遠隔地にある物(あるいは人)があたかも近くにあるかのように感じながら、操作などをリアルタイムに行う環境を構築する技術およびその体系のこと

テレイグジスタンス - Wikipedia

端的に言えば、遠隔地に存在するロボットなどの装置にユーザーがなんらかの方法で乗り移って、まるで自分がその場にいるかのように振る舞うことができるようになる技術のことです。この技術により、例えば、危険な地域(有毒ガス地帯、放射線量が高い地域、宇宙空間等)に実際に人間が赴くことなく、ロボットから送られてくる五感情報を通してスムーズに作業ができるようになったり、なんらかの理由により家から出ることができない方がロボットを通して社会で活躍したりイベントに参加したりできるようになるのです。

システム構成

システム構成は以下のようにしました。

f:id:yubeshineko:20191030172512p:plain

Unity, Raspberry Pi間はネットワークを通じて、Raspberry Pi, Arduino間はUSBケーブルで繋がっています。

実行環境

作業

ステップ1: ラズパイのカメラモジュールの映像を配信する

ラズパイを使ってカメラの映像を配信するとなったら、一般的にはmjpg-streamerが使われることが多いです。今回のプロジェクトでもこちらを使います。

カメラモジュールの設定についてはこちらが分かりやすいかと思います。

www.pc-koubou.jp

次にmjpg-streamerをインストールします。今回はラズパイのカメラモジュールに対応している派生版のmjpg-streamerを使用します。

github.com

# 準備
$ sudo apt-get install cmake libjpeg8-dev
# もしgcc, g++が入ってなければ
$ sudo apt-get install gcc g++

# インストール
$ git clone https://github.com/jacksonliam/mjpg-streamer.git
$ cd mjpg-streamer-experimental
$ make
$ sudo make install

これでmjpg-streamerを利用できるようになりました。カメラモジュールが接続されていることを確認して利用してみます。

# カメラモジュールが接続されているか確認
$ vcgencmd get_camera
# "supported=1 detected=1" と表示されたら、カメラが認識されている

$ pwd
/[mjpg-streamerをインストールしたフォルダまでのパス]/mjpg-streamer/mjpg-streamer-experimental

$ export LD_LIBRARY_PATH=.
$ ./mjpg_streamer -o "output_http.so -w ./www" -i "input_raspicam.so"

これでカメラの映像が配信されます。ラズパイと同じネットワークに接続されているデバイスでブラウザを開いてhttp://[ラズパイのIPアドレス]:8080/にアクセスするとmjpg-streamerのデモページが開き、左側のメニューからStreamを開くと、現在のカメラの映像が見れます。

しかし、毎回長い実行コマンドを打つのも大変なので、実行する際のシェルスクリプトを書きます。お好きなエディタで以下のファイルを作成し、mjpg-streamer-experimentalフォルダ以下に保存します。

mjpg_streamer_boot.sh

#!/bin/sh
 
# mjpg-streamer start script
 
# Path to mjpg_streamer and libraries
export LD_LIBRARY_PATH="/[mjpg-streamerをインストールしたフォルダまでのパス]/mjpg-streamer/mjpg-streamer-experimental"
STREAMER="$LD_LIBRARY_PATH/mjpg_streamer"
 
# Pi camera configurations
XRES="640"
YRES="480"
FPS="30"
 
# Web configurations
WWWDOC="$LD_LIBRARY_PATH/www"
PORT="8080"
 
# Start streaming
$STREAMER -i "input_raspicam.so -x $XRES -y $YRES -fps $FPS" \
          -o "output_http.so -w $WWWDOC -p $PORT" \
      -b

$ ./mjpg_streamer_boot.shで配信を開始することができます。なお、オプションでバックグラウンドで動作するようにしています。これは後にpythonコードを走らせるためです。

プロセスをkillする際は以下のようにします。

$ pgrep mjpg
# mjpg-streamerのプロセスIDが表示される
$ kill -9 [mjpg-streamerのプロセスID]

オプションについての説明は省略します。色々あるので調べてみてください。

ステップ2: ラズパイが配信している映像をUnityで受信する

次に、ラズパイで配信している映像を受信するためのUnityでの処理が必要になります。ネットで何かしらの方法がないものかと調べていたところ、ぴったりのものを見つけました。

hammmm.hatenablog.com

こちらの方が作成されたアセットを利用させていただきます。なお、このアセットはAndroidWindows 32/64bitでしか動作が確認されておらず、私がMacでやったときは動作しませんでした。

  1. アセットをダウンロード

    上記のページからダウンロードページに飛んでダウンロードします。

  2. ダウンロードしたunityパッケージをProjectにインポートする

  3. シーンにオブジェクトを配置(Quadなど)
  4. 配置したオブジェクトにMJStreamingPlayerスクリプトをアタッチする
  5. MJStreamingPlayerコンポーネントServerUrlフィールドにサーバーのURLを書く。

    http://[ラズパイのIPアドレス]:8080/?action=streamとなります。

  6. マテリアルを作成し、ShaderをUnlit/Textureに変更。このマテリアルを先ほど追加したオブジェクトにD&D。

これで準備はできました。ラズパイでmjpg-streamerを起動しておいてからUnityで実行してみます。Unityまで画像が来ていることが確認できたら成功です。

ステップ3: HMDの角度をサーボモーターで扱える値に変換してラズパイに送信する。

ラズパイに繋がったカメラの映像をUnityで見ることができました。次はUnityでのHMDの傾きをラズパイに送る処理を実装します。HMDはHTC Viveを使いました。

まずはHMDを利用できるようにします。

Project Settings > Player Settings > XR Settingsで Virtual Reality Supportedにチェックを入れてVirtual Reality SDKsOpenVRを追加します。これでカメラオブジェクトの映像がHMDに映るようになります。

あとはカメラオブジェクトに自身の回転情報を取得するスクリプトを書いてあげてそれをラズパイに送ればそれでOK!

...というわけにはいきません。なぜなら、サーボモーターでの回転角度と、Unity内での回転の角度が一致していないためです。そのため、ラズパイに回転角度を送る前にUnityで角度の変換をする必要があります。また、サーボモーターは180度回転のサーボを利用しています。

実際には以下のような違いがあります。

f:id:yubeshineko:20191031013819j:plain

transform.localEulerAnglesをサーボの回転角度に変えていきます。

Unityの角度 -> サーボの角度の変換コード。

ヨーに関してですが、HMDのY軸角度がサーボで回ることができない90度 ~ 270度の範囲にある際は、90~180の間の時は0度に、180~270の間の時は180度に制限しています。最初は360度回るサーボモーターでやってみたのですが、360度サーボをArudinoのサーボモーターライブラリで動かすことができなかったので今回は断念しました。

次に、ラズパイに変換した角度データを送信するスクリプトを書きます。通信にはUDPを用います。

Udp通信をするためのスクリプト。

そして、この2つのクラスを利用するスクリプトを書いてあげます。

UserDataManager.cs

using UnityEngine;

public class UserDataManager : MonoBehaviour
{
    public GameObject cameraObject;
    public string remoteHost = "";
    public int remotePort = 60000;
    UdpSender udpSender;
    HeadRotation headRotation;

    void Start()
    {
        udpSender = new UdpSender(remoteHost, remotePort);
        headRotation = cameraObject.GetComponent<HeadRotation>();
    }

    void Update()
    {
       udpSender.SendData(headRotation.GetServoAngle()); 
    }

    void OnApplicationQuit()
    {
        udpSender.UdpClientClose();
    }
}

3つのスクリプトが書けたら、カメラオブジェクトにHeadRotationスクリプトUserDataManagerスクリプトをアタッチします。アタッチしたら、UserDataManagerコンポーネントのフィールドを適切に設定していきます。

CameraObjectにはカメラオブジェクトをD&D、RemoteHostはラズパイのIPアドレスとします。RemotePortは今回は60000にしておきます。

準備ができたら、ラズパイで以下のコマンドを打ってパケットのキャプチャをしてみます。

# tcpdumpがなければ
$ sudo apt-get install tcpdump
$ sudo tcpdump -A -n udp port 60000

Unityにてシーンを再生して、正しくパケットがキャプチャされていれば成功です。

最後に、スクリーンとなるオブジェクトをVRカメラの子にしてHMDの正面に張り付くようにしてあげましょう。

ステップ4: Unityから送られてきたサーボモーターの角度データを受信してArduinoに送信する

ラズパイでUnityから送られてきたデータを受信することができましたので、次は送られてきたデータを受け取って、Arduinoに渡すためのPythonスクリプトを書きます。

ArduinoとラズパイはUSBケーブルで繋がっています。そのため、シリアル通信用のライブラリが必要になります。今回はpyserialというライブラリを使用することにします。pyserialはpipを使ってインストールできます。

# pipがなければ
$ sudo apt-get install python-pip
$ pip3 install pyserial

また、ラズパイのシリアル通信を有効にしていないのであれば有効にしてあげる必要があります。

$ sudo raspi-config

pyserialがインストールできたらコードを書いていきます。

Unityから送られてくる角度データ受信し、Arduinoに送信するスクリプト。

スレッドを利用してUDP通信部とシリアル通信部を同時に実行しています。ちなみに、32行目の/dev/ttyACM0ついて、基本的にはこれで問題ないのですが、この部分は人によっては違うことがあるかもしれません。その際はこの部分を書き換えてあげる必要があります。

ステップ5: ラズパイから送られてきたデータを処理してサーボを制御する

いよいよサーボを制御する段階まできました。というわけで、早速スクリプトです。

ラズパイから送られてきたサーボ角度のデータを処理してサーボを制御するコード

38行目でSerial.setTimeout(20)としてタイムアウトの時間を短くしています。デフォルトの値は1000(ms)となっており、今回のような速さが求められるプロジェクトにおいて1秒では大きすぎるので20msとしてあります。

また、送られてくるデータは{ピッチの角度},{ヨーの角度}となっているため、それぞれの角度を取り出さなくてはなりません。どうやら、Arduinoには文字列を区切り文字で分割するsplit関数は用意されていないとのことなので、以下の方が作成されたsplit関数を利用させていただきます。

algorithm.joho.info

コードが書けたら回路も作ります。

f:id:yubeshineko:20191031134241p:plain

サーボモーターの電源は外部から取ります。私の場合、12V1AのACアダプターを利用しました。また、サーボモーターの定格電圧は4.8V~5Vとなっているので、5Vの三端子レギュレーターによって5Vを作り出してサーボモーターに給電しています。

カメラマウントの組み立てについてはこちらをご覧ください。

studio.beatnix.co.jp

ヨー制御用のサーボとカメラマウントの間に微小のスペースが空いていて少しぐらつくので、スペーサーを挟んで接着剤で固定したりするといいかもしれないです。カメラの設置には両面テープなどを使用します。また、サーボを動かす際はカメラマウントは固定しないとかなり動きますので、何かしらの方法で固定する必要があります。

ステップ6: 全体テスト

最終チェックをします。

  1. mjpg-streamerを起動する
  2. Arduinoにコードを書き込む
  3. ArduinoとラズパイをUSBケーブルで接続する
  4. ラズパイでPythonコードを実行する

    $ python3 telexistenceApp.py

  5. Unityでシーンを再生する

シーンを再生して、HMDサーボモーターの角度が一致していることが確認できたら成功です!

ちなみに、Viveでやった後にOculus Questでも動作することを確認しました!

まとめ

今回は遠隔のカメラ映像をHMDを通して見て、HMDの傾きを遠隔のカメラにも適応させることで簡易的なテレイグジスタンス体験を味わうことをやってみました。しかし、ただ遠隔地のカメラ映像を見るだけではテレイグジスタンスとしては不十分です。そこで、次はロボットアームを導入したりして遠隔地の対象とのインタラクションができるようにしてみたいですね。また、今はまだ同一のネットワークでしか利用できないので、外部ネットワークからのアクセスをできるようにもしてみたいです。他にも、サーボモーターの制御にて、writeMicroseconds()関数を利用することで角度を指定するよりも細かくサーボの角度を制御できるので、これについてもやってみたいです。

エラーなど

C#でのUdpClientについて

UdpClientでのデータの送信方法としては

UdpClient udp = new UdpClient ();
udp.Send (sendBytes, sendBytes.Length, remoteHost, remotePort);

とする方法と

UdpClient udp = new UdpClient (remoteHost, remotePort);
udp.Send (sendBytes, sendBytes.Length);

とする方法があるのだが、後者のほうでやろうとすると、最初の1度はデータが送られるのに次からはデータが送られずにエラーを吐く。SocketExcepthonをキャッチしてエラーコードを確認したところ、コードは10061であった。接続対象から接続が拒否されているらしいが、原因は全く持って不明。

https://support.microsoft.com/ja-jp/help/819124/windows-sockets-error-codes-values-and-meanings

Python2系と3系でのpyserialの違い

pyserialにてPython2系と3系で微妙な違いがあった。 シリアル通信の際に 2系では

ser.write("Hoge")

とすればすぐにできるが、3系では

ser.write(str.encode(“Hoge”))

としないとできないとのこと。

stackoverflow.com

参考

mjpg-streamer

https://blue-black.ink/?page_id=5298

https://blue-black.ink/?page_id=2245

PythonでのUDP通信

https://www.shujima.work/entry/2018/07/13/195100

C#でのUDP通信

https://dobon.net/vb/dotnet/internet/udpclient.html

シリアル通信について

https://www.arduino.cc/reference/en/language/functions/communication/serial/readstring/

https://karaage.hatenadiary.jp/entry/2015/06/10/080000

https://novicengineering.com/シリアルモニターの使い方【arduino】/

Pythonでのスレッドの止め方

https://codeday.me/jp/qa/20190711/1225730.html

UnityのC#でインターフェースを使う

本ブログ初のVRコンテンツに関する内容です。春休み中に設計に関する技術書を読んだので実践してみようと思い作ってみました。

読んだ本はこちらです

Adaptive Code ? C#実践開発手法 第2版

テストやリファクタリングの仕方からSOLID原則、依存性の注入など大規模な開発に不可欠な要素が学べる素晴らしい書籍となっておりました。特にインターフェースの有効性について学べたのが一番良かった点ですね。

というわけで、今回は「インターフェースを利用した抽象化をしてみる」という目的でVRコンテンツを作ってみました。

成果物

コントローラーの加速度を読み取って魔法を放出し敵を倒すゲームとしました。

クラス図

f:id:yubeshineko:20190406115208p:plain

今回が初めてなので正直なところ書き方がこれで合ってるかは分からないですが書いてみました。注目点はインターフェースや抽象クラスを使った抽象化です。将来的にどのポイントが拡張されるかを考えてそこを拡張できるように抽象化しました。例をあげるとプレイヤーの入力をIInputProviderインターフェースとして抽出してあげることでテスト入力がしやすくなったり違うコントローラーでも使えるようにしました(今回はテスト入力用のクラスは作りませんでしたが...)。Leap Motionを使った何かをやってみたかったのでいずれ暇があればハンドジェスチャで魔法を飛ばしたりできるようなIInputProviderインターフェースの実装をしてみたいと考えていたりもしてます。

他にはタグの代わりにインターフェースを使うということもやってみました。ダメージを与える対象のオブジェクトにIDamageApplicableインターフェースを実装したスクリプトをアタッチして与ダメージオブジェクトが被ダメージオブジェクトに衝突した際に与ダメージオブジェクトが衝突したオブジェクトからIDamageApplicableインターフェースを取得します。取得できたら被ダメージオブジェクトのApplyDamage()を実行といった形になります。開発の最初の段階でダメージを与える際のメソッドの引数はこうだよ!とインターフェースを指定しておくことで敵の種類が増えた時に引数が増えてしまい上位モジュール(今回の場合、Magicクラス)に変更が生じる...なんてことがなくなります。これはSOLID原則の「依存性反転の原則」, 「オープン/クローズド原則」を守るための工夫でもあります。今回の例で考えるとMagicオブジェクトがSkeletonオブジェクトに衝突した際にMagicクラスがGetComponent<IDamageApplicable>()を実行し、取得できたらSkeletonオブジェクトのApplyDamage()を実行するといった感じですね。

void OnTriggerEnter (Collider other)
{
    var enemy = other.gameObject.GetComponentInParent<IDamageApplicable> ();
    if (enemy != null)
    {
        enemy.ApplyDamage (damage);
    }
}

感想

今回初めてまともに設計してみたんですが、やっぱり最初は難しいですね。特に名前空間の名前決めや各クラスをどの名前空間に所属させるかなどは悩みどころでした。もしかしたらゲームのジャンルごとにある程度テンプレート的なものがあったりするのでしょうか? こればっかりは慣れるしかないのでしょうね。

それと、最初の段階でどこまで拡張できるようにするかをしっかり決めることも大事です。最初の段階ではIDamageApplicableインターフェースは以下のようになっていました。

namespace WCE.Damages
{
    public interface IDamageApplicable
    {
        void ApplyDamage(Attribute attribute);
    }
}

これは魔法の属性だけで敵に与えるダメージ計算をすることを想定したインターフェースでした。しかし、開発の途中で「プレイヤーがアイテムを拾って攻撃力が上がったら?」 なんてことを考えた結果、開発の途中でインターフェースを書き換えるという禁忌を犯すことになりました。これはSOLID原則の1つであるオープン/クローズド原則に違反する行為であり、あってはならない事態でした。今後はこのようなことがないよう初めの段階でキチンと設計をしておくようにしたいです。

あと、モチベーションが続くボリュームにすることが大事ですね。開発当初は敵の種類や魔法の属性をたくさん考えていたりプレイヤーのステータス上昇アイテムなどを考えていたのですが、途中で心がポッキリ折れてしまったので結局遊べるところまで持っていって終わろうということにしました。インターフェースで拡張ポイントは確保してあるのでまたやる気が出たら拡張することはできるんですけど当分はやれそうもありません。

それでも「インターフェースを使って抽象化をする」ことは実践できたので満足ではあります。

LINE Clovaに地域のゴミ出しの方法をしゃべってもらう

スマートスピーカーっていいですよね。スマホみたいに画面を開いたりすることなく情報をインプットできるので。

今回はとあるハッカソンでLINE Clovaを取り扱うことがあったのでその時に作ったClovaのスキルについて書いていきます。

システム構成

f:id:yubeshineko:20190225152208p:plain

Clovaに知りたいゴミの捨て方を聞くとその情報が返ってくるという感じですね。ゴミの捨て方のデータは福島県会津若松市のオープンデータである会津若松市 家庭ごみ分別辞典から持ってきました。バックエンドで使用した言語はC#です。

スロットとインテントの設定

スロットとインテントの設定は以下のようにしました。スロットとインテントの詳細については割愛します。

  • スロット f:id:yubeshineko:20190225160004p:plain 49個全てをUIから入力するのは大変なので、tsv形式のデータを用意して読み込ませました。

  • インテント f:id:yubeshineko:20190225160215p:plain

また、終了に関するワードを取り扱うためにTerminateスロットとTerminateインテントも用意しました。

...Garbageのスペル間違ってた!

スクリプト

残念ながらハッカソンの時間内では複数検索結果が見つかった場合の処理は実装できませんでした。実装するとしたら、LINEのGUIで選ばせるという方法が考えられますね。

使用したパッケージ

いづれVSCode内にてNugetを通して導入できます。なお、LINE botと連携するにはClovaスキルとMessaging APIのプロバイダーが同じである必要があります。

動作の様子

f:id:yubeshineko:20190225163338p:plain

写真なのでわかりにくいのですが、ちゃんと要求したゴミの情報を汲み取ってくれてClovaが捨て方について話してくれます。また、長い情報で聞き逃してしまっても大丈夫なようにLINEにも通知が飛んでくれます。

参考文献

マンガでわかるLINE Clova開発 1話「どんな仕組みで動くの? サーバーレスって何?」|TECH PLAY Magazine[テックプレイマガジン]

ハッカソンで大失敗した話を晒す

2月下旬に某所で開かれた某ハッカソンに参加したのですが、そこでハッカソンで考えられる全ての失敗を犯したと思うので、今後の自分の戒めとして記録に残しておこうと思います。

開発しようと思ったコンテンツ

VRゲームってプレイヤーだけしか楽しめないじゃないか...

そんな思いからこんなコンテンツを考えつきました。

センサを装備したプレイヤーが何かしらのジェスチャーをすることでVR空間内にジェスチャーに対応する変化が現れる...みたいな。例えば三人のプレイヤーがいるとすると...

  1. プレイヤー1の前にゴブリンがあらわれた!
  2. ゴブリンの色が赤(炎属性)だ!
  3. プレイヤー1には炎属性のゴブリンに対応する武器が必要だ!
  4. プレイヤー1がプレイヤー2とプレイヤー3に武器(水の剣)を要請
  5. センサを持ったプレイヤー2とプレイヤー3はそれぞれ剣を生成するジェスチャ、水属性を付与するジェスチャを実行
  6. プレイヤー1の上空から水属性が付与された剣が降ってくる
  7. プレイヤー1が剣を手にとってゴブリンを倒す!

というような流れを実装したいと考えていました。

チームメンバー

私は敵周りおよびUnityとマイコンのパイプ部分を担当しました。

技術的仕様

  • Unityのバージョン : 2018.3.5f1
  • HMD : HTC Vive
  • マイコン : Arduino Mega
  • センサ : 加速度センサとカラーセンサ。前者が武器、後者が属性を表す

具体的なコンテンツ仕様

f:id:yubeshineko:20190222174828p:plain

f:id:yubeshineko:20190222174854p:plain

  • 武器は剣、魔法の杖、銃の3種類(1回きりの使い捨て)
  • 敵はスライム、オーク(白板ではゴブリン)、鳥系の3種類
  • 属性は炎、草、水の3種類(●ケモンとは関係ありません...多分)
  • スライムなら魔法の杖、オークなら剣、鳥系なら銃が対象の武器となる
  • 武器はプレイヤーの座標の上空に降ってくる
  • 敵はそれぞれ特有の移動をする
  • 弱点属性だと一撃で倒せるが、弱点属性でない属性がついた武器ではあまりダメージが入らない
  • プレイヤーはワープでの移動が可能

ということを実装するつもりでした。(雲行きが怪しい...)

結果

大失敗!

今回のこの企画は私が考えたものでしたので、自動的に私がディレクターとして開発を進めることになっていたのですが、私の失敗のためにチームに多大な迷惑をかけることになってしまいました。結果、ハードウェアの部分は完成してUnityに任意のデータを送るまではできたのですが、コンテンツ部分が完成せず、デモができないという結果なってしまいました。ハッカソン終了後に一人反省会をしまして、何がいけなかったのかをしっかり考えてみたのですが、様々なミスを犯していたことに気づきました。

うまくいかなかった原因

  • コンテンツ設計

    • 属性、形状など考慮すべき点がかなり多かったため開発が大変になってしまった

      実は今回のハッカソンが私にとってまともに参加した初めてのハッカソンでした。チームでの開発経験もあまり豊富であるとは言えませんし、ましてや開発の主軸となった経験もありませんでした。そんな状態で決して単純とは言えない今回のコンテンツの仕様を考えてしまったのが失敗の原因その1だと考えられました。もちろん自身の開発力の無さも原因の1つではあるのですが、それを考慮した上で仕様を練ることも必要だったのではないかと思います。

  • チームオペレーション

    • 問題点や進捗の共有をうまく行えていなかった

      一応進捗報告はチームのSlackチャンネルで確認を取っていたのですが、作業をしているうちにその進捗内容が他のメッセージの中に埋もれてしまって出来上がったモデルやアニメーションなどに気が付かなかったなどということがあったりしました。また、メンバーの誰がどんな作業をしているのかが分からなかったということもありました。

      他にも、もう一人のUnityプログラマーが、Unityの最新バージョンにてViveコントローラーの入力の取得方法が今までのバージョンと違い苦労しているということに早く気付くことができず、1日目の深夜(1:00)頃にようやくそのことに気付いてバージョンを下げたなんてこともありました。

      これについてはTrello等でカンバン管理をして進捗の状況把握や問題点の把握をすると良かったのかなという感じです。

  • 開発

    • まともに使ったこともないのにクラスの継承を意識したクラス設計をしてしまい、後半で詰まってしまった

    • クラス設計をおろそかにしてしまい、後半においてゲーム進行のロジック作成時に詰まってしまった

      当初は敵の動きの実装を以下のような感じで実装してみようと考えていました。

      f:id:yubeshineko:20190222195824p:plain

      Slime, Bird, OrcはEnemyBaseを継承しているという形です。

      これで敵単体の動作はできたのですが、問題は属性に関わる部分でした。本来ならば自身の属性を表す変数なりプロパティなりをEnemyBase抽象クラスが持っておくべきだったのですが、それの必要性を後半まで気付くことができませんでした。結果、敵を生成する際のロジック、攻撃された武器の属性判定からのダメージ計算ロジックの設計に戸惑ってしまいました。他にも、GetCompont<EnemyBase>()でちゃんと派生クラスのデータを持ってこれるのかなども分かっていませんでした。このため、敵の生成ロジック記述時に時間を食ってしまいました。

    • 作れもしないのに時間ギリギリまで作業をしてしまって結果的にプレゼンに割く時間を完全に無くしてしまった

      個人的ハッカソンでやってはいけないことワースト1位。終了10分前くらいで既に完成は無理だろうと断言できるような状態だったのですが、結局開発終了までディスプレイとにらめっこしていました。そのため制作物プレゼンテーションで使うスライドの制作がまともにできませんでした。幸いチームメンバーの一人が作ってくれてはいたのですが、その人は他のハッカソンにも参加しており、そちらが忙しくなってしまったので最終的に中途半端なスライドのままプレゼンをすることになり、コンテンツの企画内容や苦労した点、工夫した点などについて十分に伝えることができませんでした。

    • 土台を作ることよりも小さいところに気を払いすぎてコアの部分となるゲーム進行ロジックをまともに作れなかった

      個人的ハッカソンでやってはいけないこと同率ワースト1位。優先順位が狂っていたということですね。1日目の後半で敵の動きの実装をしてる最中にQuaternionで詰まってしまってそこでかなり時間を費やしてしまい、代償としてコアとなるロジックの実装にかける時間を溶かしてしまいました。冷静に考えてみれば、敵の動きの実装なんかよりもゲームマネージャーの実装の方がはるかに重要度は高いことは言うまでもないですよね。

    • 早い段階で妥協点を模索して遊べる段階まで持っていくことが最優先であったはずなのに、最初の仕様に最後までこだわってしまった

      これに関しては、最初の段階で実装でかかる時間や重要度の見積もりをしたり、実装する必要があるものを列挙したりするべきだったと思います。そうすればどの作業を優先的に行えばいいか分かり、あまり重要ではない部分で悩んで時間を取られることはなかったと思われます。他にも、早い段階でコンテンツの複雑さを理解でき、仕様変更することを考えることができたのではないかと思います。

まとめ

審査員の一人がハッカソンの講評でこんなことを述べていました。「ハッカソンでのコツは最初にプレゼン用の資料を作り、チームでどんなコンテンツを作るかを最初に共有して、土台を作ることを最優先でやることだ」と。本当にその通りだと実感しました。細々としたところもそれはそれで重要であるとは思いますが、結局一番大事なのは動くものを見せることなんですよね。今回のハッカソンではそのことを痛感しました。

また、ハッカソンで今までやったことがない新要素を取り入れることはいいことだと考えているのですが、それをするなら仕様は単純の方がいいです(特に今回のような短い期間のハッカソンではなおさら)。切羽詰まって開発することもなくなりますし、デモをする時間も多く取れるようになりますからね。何より、心のゆとりが生まれてハッカソンを楽しむことができるようになります。

この失敗を次の機会に大いに生かしていこうと思います。

UnityからMQTTブローカに接続し、メッセージをUIとして表示させる

MQTTについて色々やっているうちに「UnityからMQTTブローカに接続する方法があるのだろうか」という思いに駆られてしまったので、実践してみることにしました。今回はそのことについて書いていきます。

実行環境

  • Mac OS 10.14.2
  • Unity2018.3.4f1

Unityの.NETのバージョンは4.xとします。

使用するMQTTクライアントライブラリについて

C#用に作られたMQTTクライアントのライブラリにはいくつか種類がありますが、有名どころだとM2MqttかMQTTnetあたりになるようです。双方のGitHubのコミット履歴を見てみたところ、MQTTnetの方が開発がアクティブであったため、今回はMQTTnetを使ってやっていくことにします。

UnityにDLLファイルをインポートする

NuGet GalleryからMQTTnetの最新の安定バージョンである2.8.5をダウンロードします。ダウンロードはこちらから。ダウンロードされたデータ名はmqttnet.2.8.5.nupkgとなっていますが、このままでは展開できないので拡張子を.nupkgから.zipに変えて展開します。展開後、mqttnet.2.8.5 > lib > net472 中にある MQTTnet.dll を使用するUnityプロジェクトのAssets/Plugins/以下にインポートします。これでMQTTnetをUnity内で使用する準備ができました。

実装

この記事での具体的な実装としては、UnityからMQTTブローカに接続、SubscribeしてPublishされたメッセージをUnity内でテキストとして表示させるということをします。テキストの表示にはTextMeshProを利用しました。また、メッセージを受信した際のコールバック(ApplicationMessageReceived)内で受信したメッセージを他クラスに渡せるようにしています(UniRxの機能の一部を利用)。

非同期処理に注意

MQTTController.csの20行目と67行目に注目すると普段あまり見慣れないことが書いてありますね。

var context = SynchronizationContext.Current;

//            ~~~~  略 ~~~~

context.Post (_ =>
            {
                OnMessageReceived.OnNext (message);
            }, null);

実はUnity固有の処理はメインスレッド以外から実行することができません。これに気付かなくて1時間くらい表示されないテキストを前にして唸ってました...

MQTTnetの内部ではたくさんの箇所で非同期処理が行われており、そのほとんどの処理においてconfigureawait(false)となっています(こちらを参照)。結果、MQTTController.cs内ではスレッドIDが刻々と変化してしまい、Unity固有の処理をできなくなる...となるわけです。実験としてMQTTController.csの各所にスレッドIDを表示するログを出力するようにしてみます。

async void Start ()
    {
        var context = SynchronizationContext.Current;
        Debug.Log ($"はじめ : {Thread.CurrentThread.ManagedThreadId}");

        //   ~~~~ 略 ~~~~

        mqttClient.Connected += async (s, e) =>
        {
            Debug.Log ("MQTTブローカに接続しました");
            Debug.Log ($"ブローカ接続後 : {Thread.CurrentThread.ManagedThreadId}");
            await mqttClient.SubscribeAsync (
                new TopicFilterBuilder ()
                .WithTopic ("your/Subscribe/Topic")
                .Build ());
            Debug.Log ("指定したトピックをSubscribeしました");
            Debug.Log ($"Subscribe後 : {Thread.CurrentThread.ManagedThreadId}");
        };

        //   ~~~~ 略 ~~~~

        mqttClient.ApplicationMessageReceived += (s, e) =>
        {
            var message = System.Text.Encoding.UTF8.GetString (e.ApplicationMessage.Payload);
            Debug.Log ($"メッセージ受信 : {message}");
            Debug.Log ($"メッセージ受信後(context.Post以前) : {Thread.CurrentThread.ManagedThreadId}");
            context.Post (_ =>
            {
                Debug.Log ($"メッセージ受信後(context.Post内) : {Thread.CurrentThread.ManagedThreadId}");
                OnMessageReceived.OnNext (message);
            }, null);
        };

結果

f:id:yubeshineko:20190210025651p:plain

確かに、各処理の後においてスレッドIDが異なっていることが分かります。そして、context.Post内ではちゃんとUnityのメインスレッドに戻っていますね。試しにcontext.Postの部分をコメントアウトして実行してみると、テキストが表示されなくなります。

完成品

ちょっとした遊び心で「ぬこ」というメッセージをPublishすると猫が現れるようにしました。

参考記事

MQTTnetをUnityで使えるようにする

MQTTnet を Unity で使う - Qiita

うまくいかなかった原因解明のきっかけ

Oculus GoとNode-REDでMQTTをやり取りするメモ – 1ft-seabass.jp.MEMO

非同期処理とSynchronizationContextについて

【Unity開発者向け】「SynchronizationContext」と「Taskのawait」 - Qiita

Unity2017で始めるTask(async~await) - Qiita

AlexaからESP32につながったLEDを操作した話

去年のアマゾンのサイバーマンデーにてEcho dotが通常価格5,980円のところ3,240円で売られていたのでつい衝動買いしていたことを思い出したのでESP32との連携をして遊んでみることにしました。

目次

実行環境

  • OS : macOS Mojave 10.14.2
  • Arduino IDE : 1.8.5
  • ESP32 : ESPr Developer 32(SWITCH SCIENCE)
  • Echo dot : 第3世代

目標

AlexaからESP32に接続されたLEDを操作する。

f:id:yubeshineko:20190108005339p:plain

準備

ソフトウェア領域

  • ESP32の開発環境
  • ライブラリ各種(下記参照)
  • Alexaのスマホアプリ

ハードウェア領域

  • LED(ここでは赤色を利用しました)
  • 抵抗(LEDの色に合わせた大きさを利用しましょう。ここでは330Ωを利用しました)
  • ESP32
  • Echo dot

使用したライブラリ

FauxmoESPというライブラリを利用させていただきました。私の環境で実行した時のバージョンは3.1.0でした。

xoseperez / fauxmoESP — Bitbucket

READMEを見てみると、依存関係にあるライブラリがあるそうです。ESP8266とESP32で違うそうなので、ESP32用の依存ライブラリも入れておきましょう。

$ cd your_ArduinoDir/libraries
$ git clone https://bitbucket.org/xoseperez/fauxmoesp.git
$ git clone https://github.com/me-no-dev/AsyncTCP.git

コードを書く

ライブラリのサンプルコードを少し変更したものです。

#include <Arduino.h>
#include <WiFi.h>
#include "fauxmoESP.h"

fauxmoESP fauxmo;

const char *ssid = "****************";
const char *password = "****************";
const int ledpin = 26;
const char *id_led = "led";

void WifiSetup()
{
  WiFi.mode(WIFI_STA);
  Serial.printf("connecting to %s¥n", ssid);
  WiFi.begin(ssid, password);

  while (WiFi.status() != WL_CONNECTED)
  {
    Serial.print(".");
    delay(100);
  }

  Serial.println();
  Serial.print("WiFi connected: ");
  Serial.println(WiFi.localIP());
}

void setup() {
    Serial.begin(115200);
    pinMode(ledpin, OUTPUT);
    digitalWrite(ledpin, LOW);
    WifiSetup();

    fauxmo.createServer(true);
    fauxmo.setPort(80);  //※
    fauxmo.enable(true);

    fauxmo.addDevice(id_led);

    fauxmo.onSetState([](unsigned char device_id, const char *device_name, bool state, unsigned char value) {
      Serial.printf("Device #%d (%s) state: %s value: %d\n", device_id, device_name, state ? "ON" : "OFF", value);

      if (strcmp(device_name, id_led) == 0) {
        digitalWrite(ledpin, state ? HIGH : LOW);
      }
    });
}

void loop() {
    fauxmo.handle();
}

fauxmo.setPort(80);のところですが、第3世代のEcho dotを使う場合は使用するポート番号は80番でないといけないようです。

回路を組む

ESP32の26番ピン - LED - 抵抗 - GND となるようにつなぎます。よくあるLEDの回路ですね。

Alexaのスマホアプリを開いてESP32を認識させる

ここでEcho dotに電源を接続してWiFiに接続します。

f:id:yubeshineko:20190107234724p:plain

  1. 下のメニューからデバイスを選択して右上の「+」のアイコンをクリックします。
  2. 下からメニューが出てくるので「デバイスを追加」を選択します。
  3. セットアップするデバイスの種類を選ぶ画面に移動したらすべてのデバイス > その他(一番下にあるのでスクロールします)を選択します。
  4. 「デバイスを検出」をタップします。ここで数秒待ってESP32を検出します。

f:id:yubeshineko:20190107235526p:plain

バイスが検出されたら上の画像のようになるはずです。「デバイスをセットアップ」をタップしてデバイスを登録します。その後、fauxmo.addDevice();の引数として渡した文字列がデバイス名として登録されます。今回は"led"という名前で登録されますね。ここまでやれば作業は終了です。

完成

「アレクサ ledつけて」と言うとつけてくれます。反対に「アレクサ led消して」と言うと消してくれます。

赤外線通信用ライブラリとEcho dot

赤外線通信をして音声で家電操作をしたいと考え、赤外線通信用ライブラリであるIRremoteをこれと試したのですが、実行中にGuru Meditation Error: Core 1 panic'ed (LoadProhibited). Exception was unhandled.などと言うエラーを吐かれてしまいました。espressifの公式サイトによるとこう書かれていました。

This CPU exception happens when application attempts to read from or write to an invalid memory location.

ふむふむ、不適切なメモリ領域にアクセスしてしまい、パニックを起こしていると...。ESP8266とEcho dotの組み合わせで赤外線通信をしている例があったので、どうしても赤外線を飛ばしたいというならそちらを使うか、ライブラリを使わずに赤外線を飛ばすように実装するしかないですね。いづれはこちらも実装してみたいものです。

参考サイト

ESP32をAmazon Alexaのスマートホームのデバイスに登録する方法 - クラクスの記録帳

LINEから家のエアコンを操作する with ESP32

今回はネットワーク上のトリガーによってローカルでアクションを起こすということをやっていきます。ということで、前からずっとやってみたかったLINEから家のエアコンの操作するシステムを作ってみました!

目次

実行環境

システム構成

f:id:yubeshineko:20190105133041p:plain

トリガーとなるワードが送信されたことが確認されたらCloudMQTTにPublishするコードをHerokuにデプロイします。バックエンド側のスクリプトはGoで書きました。

CloudMQTTについて

数あるMQTTブローカのうちの1つ。Herokuのadd onとして利用できるサービスですが、CloudMQTT単体でもサービスを提供しているようです。いくつかの料金プランが用意されていますが、今回は無料プランであるCut Catを使います。無料プランには以下の制限がありますが個人で遊ぶ程度なら十分です。

  • 5 connections
  • 5 users
  • 5 acl rules
  • 0 bridges
  • No kinesis integration
  • API now throttled, 30 req/minute

準備

ソフトウェア領域

  • LINEアカウントとLINE developersへの登録
  • HerokuアカウントとHerokuCLIがインストールされた環境
  • Go言語で開発する環境
  • Arduino IDEのインストール
  • 赤外線データ(データの取得にはこちらを利用しました)
  • Arduinoライブラリのインストール(括弧内は実行時バージョン)
    • PubSubClient(2.7.0)
    • IRremote(2.2.3)

    IRremoteについては本家ではESP32での赤外線送信は対応していないため、こちらの方のものを使わせていただきます。

$ cd your_Arduino_Dir/libraries
$ git clone https://github.com/SensorsIot/Arduino-IRremote.git IRremote

ハードウェア領域

知識

作業

1 LINE チャネル作成と設定。

プロバイダーの作成は省略します。チャネルはMessaging APIとして作成し、プランはDeveloper Trialとします。チャネル基本設定画面まで行ったらChannel Secretアクセストークンをどこかにメモしておきましょう。その後以下の設定をします。

  • メッセージ送受信設定

    • Webhook送信 : 利用する
  • LINE@機能の利用

    • 自動応答メッセージ : 利用しない
    • 友だち追加時あいさつ : 利用しない

    下の2つは必須ではないですが、毎回出てくると鬱陶しいのでこの設定にしておくのがオススメです。

2 Herokuにアプリをデプロイする

このアプリ内ではLINEから送られてきたメッセージの処理とCloudMQTTへのPublishを行います。

2.1 プロジェクトファイル作成
$ cd $GOPATH/src
$ mkdir [アプリ名]
$ cd [アプリ名]
$ pwd
/あなたのGOPATH/src/[アプリ名]
2.2 Herokuへのログインとその他諸々
$ pwd
/あなたのGOPATH/src/[アプリ名]
$ echo 'web: [アプリ名]' > Procfile
$ git init
$ git add .
$ git commit -m “first commit”
$ heroku login
$ heroku create [アプリ名] -b https://github.com/heroku/heroku-buildpack-go.git
$ heroku config:set CHANNEL_SECRET="[Developer用管理画面で取得したChannel Secret]"
$ heroku config:set CHANNEL_TOKEN="[Developer用管理画面で取得したChannel Access Token]"
$ heroku addons:add cloudmqtt

Procfileには、Heroku上で自分のアプリケーションを動かすためのコマンドが記述されます。Buildpackの中で自動的にgoプログラムをgo buildして go install$GOPATH/binにインストールされる仕組みになっているため、web:の後はherokuプロジェクト名を書くのが定石のようです。

ここでさっきメモしたChannel Secretアクセストークンをherokuの環境変数として登録しておきます。また、add on であるCloudMQTTもこの段階で登録しておきます。add onの登録ができたらherokuの環境変数CLOUDMQTT_URLが追加されているので$ heroku config:get CLOUDMQTT_URL環境変数を取得しどこかにコピペしておきます。デフォルトではmqtt://User:Password@Server:Portとなっているはずです。$ heroku addons:open cloudmqttでCloudMQTTのページに飛べるので環境変数と見比べてみてください。

確認すると分かるのですが、デフォルトではポートがMQTTポートになっていますね。コーディングするときのことを考えて、この部分をSSLポートに変えておきましょう。

$ heroku config:set CLOUDMQTT_URL="mqtt://User:Password@Server:SSLPort"
2.3 コードを書く

一応httpで来た時はhttpsにリダイレクトするようにしました。トピックはesp32/airconとしました。

2.4 CA証明書の取得

こちらのページよりPEM形式のルート証明書を取ってきます。テキストエディターにコピーしてca-certificates.crtとして保存しましょう。

2.5 デプロイ

パッケージ依存関係の保存をしてデプロイします。godepをインストールしていない方は$ go get github.com/tools/godepでインストールしましょう。

$ godep save
$ godep go install
$ git add .
$ git commit -m “second commit”
$ git push heroku master

最終的にはこんな構造になります。

$ tree -L 1 .
.
├── Godeps
├── Procfile
├── ca-certificates.crt
├── main.go
└── vendor
2.6 LINEのWebhook URLの設定

https://[アプリ名].herokuapp.com/callbackという形で登録します。さて、ここまで来たら第一段階突破です。友達登録を済ませたらCloudMQTTのWEBSOCKET UIを開きましょう。LINEで「つけて」「けして」と送信してみてCloudMQTTにメッセージがPublishされたら成功です。

3 ESP32のコードを書く


config.h

// Wi-FiアクセスポイントのSSIDとパスワード
const char *ssid = "your_ssid";
const char *password = "your_password";

// クライアントID(任意)
const char *clientID = "Arduino";

//CLOUDMQTT
const char *host = "CloudMQTT_Server";
const char *CloudMQTT_user = "CloudMQTT_User";
const char *CloudMQTT_pass = "CloudMQTT_Password";
const int port = CloudMQTT_SSLPort;

const char *topic = "esp32/aircon";

certificate.h

const char* ca_cert = \
"-----BEGIN CERTIFICATE-----\n" \
"MIIENjCCAx6gAwIBAgIBATANBgkqhkiG9w0BAQUFADBvMQswCQYDVQQGEwJTRTEU\n" \

 ... 略 ...

"mnkPIAou1Z5jJh5VkpTYghdae9C8x49OhgQ=\n" \
"-----END CERTIFICATE-----\n" \
;

CA証明書は先ほどのものと同じものを利用します。しかし、上記のようにArduinoに読み込ませるには少し変換が必要なようです。手打ちでやるのも面倒なのでこちらの方のプログラムを利用させていただき、証明書の変換をします。

最後にコードをESP32に書き込みます。

4 回路を組む

f:id:yubeshineko:20190105175640j:plain

本体にはUSBで給電します。ゲート抵抗は100Ωにしました。赤外線LEDには100mAまで流せるので100mA流れる想定で考えます。赤外線LEDにつなぐ抵抗ですが、赤外線LEDにかかる電圧は1.35~1.6Vらしいので、1.35Vと見積もることにしてオームの法則を適用すると大体15Ωくらいがベストだと考えられます。

完成

ESP32がCloudMQTTに接続している状況でLINEにてメッセージを送るとエアコンがつくようになります。

今後

ESP32を電池で駆動してみたいと考えていますが、どうやらWiFi接続している時はかなり電力を消費するらしいです。センサーの値を一定時間ごとに通知するなどのシステムでは一瞬だけWiFiに接続して後は解除するということができるのですがサーバに接続しっぱなしで電池駆動するとなると半日も持たないかもしれないですね...

ということで、気が向いたらそこらへんの実装をしてみたいと思います。ではでは...

参考サイト

  • Herokuアプリ

GitHub - line/line-bot-sdk-go: Go SDK for the LINE Messaging API

HerokuとGoでLINEの Messaging API環境を作ってみた - Qiita

Documentation - Go | CloudMQTT

MQTT の golang クライアントで TLS 接続を試す - Qiita

Google Home + IFTTT + Beebotte(MQTT Broker) + ESP32でLチカしてみた - Qiita

ESP8266: Connecting to MQTT broker – techtutorialsx