【Unity】M5Stackというデバイスを、UIFlowでUnityとシリアル通信してみました

# ■はじめに
**M5Stack** というデバイスを使う機会があったので、使ってみました。
また、今回は**UIFlow**というビジュアルスクリプティングツールを使用してます。


僕は、シリアル通信を今回初めて利用しましたが、**コードはそこまで複雑ではなく、簡単に実装できました。**
(セットアップなどは少し面倒でした)


# ■実行結果
こんな感じで、デバイスの加速度をUnity側に反映させることができます！
![test_2.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2419795/43cded22-0a4d-c340-a82e-16a660e01f65.gif)






# ■準備
## ●M5Stack
今回は、USB接続でやってます。
M5Stackには色々な種類がありますが、デバイス操作以外はほとんど同じです。





### 必要ツールのインストール
[DLページはこちら](https://docs.m5stack.com/en/download)


1. **DesktopIDE**
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2419795/85be720f-340f-1af1-0be7-f404de828b12.png)

1. **M5Burner**
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2419795/aa2bb713-f6cf-818c-f882-1e890cf10fca.png)



### Burnerの使い方
- ファームウェアってのを書き込むために必要っぽい。
- 使用するデバイスを探して、[**Download**]からの[**Burn**]でOK
- エラーが出る場合、UIFlowなどの接続を切断すれば解決するかもです。



### UIFlowのセットアップ
- まず、起動時に以下のような画面が表示されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2419795/93d19ca7-1233-3db9-ca95-d76b58188142.png)

    - **COM**：**シリアルポート**というもの。
        - とりあえず、選択できるものを選択すればOK
        - COMが複数ある場合は、**TeraTerm**というソフトを利用して接続確認できます。

    - **Device**：使用デバイスを選択。


<br>


- 次に、デバイスとの接続を行います。
1. USBに接続して、電源をつける
1. デバイス側で、こんな感じの画面になるように設定する(デバイスによって異なるので省略)
![IMG20230604215527.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2419795/98976470-5ddf-dd4e-faf5-f7546466a107.jpeg)

1. UIFlow側で、左下の[**リツイートみたいなアイコンのボタン**]をクリック
1. 左下に[**Connected**]と表示されたら、接続完了！！
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2419795/b6baa70e-6117-d6d0-442b-8fced0229c2c.png)



::: note warn
**接続できない場合**
- **左上の[DriverInstallation]でドライバーをインストールしてみる**
    - たぶん、一番初めに利用する場合にインストールする必要がある。
- **もう一度、Burnerで[Burn]してみる**
    - 僕はこれで解決することが多いです。
- **何度か[Refreshボタン(RTみたいなやつ)]をクリックする**
- **デバイスの設定を見直す**
- **Web版の方を利用する**
    - 最終手段です。<ins>Wifiの環境があればWebでも大丈夫です。</ins>
    - **多分WebのほうがBurnerとか使わなくてもいけるから楽かもです。**
:::



## Unity
**PlayerSettingsの「`Other Settings`」で、`API Compatibility Level`を「`.NFT Framework`」に変更する必要がある**

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2419795/82b6232b-ed1d-4db0-a2be-04cd0f6e131e.png)






# ■実装
## ●UIFlowでデータ送信機能を作る
シリアル通信に必要な機能などを紹介します。

今回は、**デバイスの傾きをUnity側に送信する**ようにしました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2419795/1baeee09-8660-bec3-acd0-24584948dd74.png)

- シリアル通信の一種である、**UART**で送信しています。
    - UART関連のブロックは、[`HardWares/UART`]に存在します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2419795/6fa2fc0d-2a48-6d1a-5d49-d17788596cac.png)




最低限必要な要素は、以下の2つです。

### 初期化
- 初期化ブロックのパラメータは、こんな感じです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2419795/6278ecb9-6b9d-3fb7-f9ef-e1190966cd5b.png)
    - 送信ピン、受信ピンに適切な値を入力。
        - <ins>**USB接続の場合、送信ピン(tx)=1,受信ピン(rx)=3**</ins>
    - ボーレートに適当な値を入力。
        - ボーレートの値が大きいほど、データ送れるらしい。
        - でも大きすぎるとエラーになるっぽい


### 書き込み
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2419795/a0afcba8-717a-2375-8820-49339c0e2294.png)

書き込みには3種類あります。今回は一番上の「**1行書き出し**」を利用しています。
今回は、**xとyの値が欲しいので、CSVのようにカンマ区切りをしたものを書き込みます。**
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2419795/b4007ea0-a3d3-fafa-f9e5-6c3e73686785.png)

:::note
今回は、送信するデータ量が少ないのでこのように1行だけで書き込みをしていますが、より多くのデータを送信したい場合は、一番下の[**write raw data**]を利用するといいかもです。
:::

---

これらの処理を追加したら、**右下の[Download]でデバイスにこのプロジェクトをダウンロードします。**
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2419795/cc322489-3452-e869-c9c1-e42662d635b4.png)

ダウンロードが完了したら、**デバイス上で傾きが表示されます。**
(※裏画面切り替え処理をしていないので、テキストがチカチカすると思います。ご了承ください。)

無事に表示されたら、UIFlow側の操作は完了です！





## ●Unity側でデータ受信する

### クラス図
クラス図はこんな感じです。
![SerialClasses.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2419795/82b22c0c-d578-3d2c-c3a4-96e709c0f5b5.jpeg)

僕のコードでは、一応きっちり継承とか使って設計していますが、
**簡易的なプロジェクトだったら、`SerialHandler`クラスとクライアントのクラスだけで大丈夫です。**


### Scene
**SerialHandler**
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2419795/71953b14-cae3-8133-16b0-f2d2bffefc8d.png)

- シリアル通信のあれこれを行う、空のオブジェクトです。
- `SerialHandler`クラスでは、シリアルポート名とボーレートを指定します。

**Player**
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2419795/411b59f3-a648-7024-dd35-55830ebc2dcc.png)

- デバイスの傾きをもとに、回転するプレイヤーです。
- 可動範囲や、向きの反転などのパラメータを用意しています。






### コード
リポジトリ↓
https://github.com/kiku09020/Unity_SerialCommunication_Test

- 以下のページのコードを参考にさせていただいています。

https://qiita.com/yjiro0403/items/54e9518b5624c0030531

#### SerialHandler
シリアル通信のあれこれを担うクラスです。

<details><summary><b><font color="YellowGreen">📄クリックしてコードを表示</font></b></summary>

```SerialHandler.cs
using UnityEngine;
using System.IO.Ports;
using System.Threading;
using System;

// データを送受信する、シリアル通信クラス
public class SerialHandler : MonoBehaviour
{
	// シリアル通信で、データを受け取った時のイベント
	public event Action OnDataReceived;

    [Header("Serial Options")]
    [SerializeField,Tooltip("開かれるポート名")] string openedPortName = "COM1";

    [SerializeField, Tooltip("開かれるポートのボーレート")] int baudRate = 9600;

    SerialPort serialPort;      // シリアルポート

    // thread
    Thread readingThread;       // 読み取り用のスレッド
    bool isThreadRunning;       // スレッド実行中フラグ

    // message
    string message;
    bool isNewMessageReceived;  // 新しくメッセージを受け取ったかどうか

    //--------------------------------------------------

    // 開始時にポートを開く
    void Awake()
    {
        Open();
    }

    void Update()
    {
        // 受け取ったら、受け取り時の処理を実行
        if(isNewMessageReceived) {
             OnDataReceived();
        }

        isNewMessageReceived = false;
    }

    // 終了時にポートを閉じる
	private void OnDestroy()
	{
		Close();
	}

	//--------------------------------------------------

	// シリアルポートを開く
	void Open()
    {
        serialPort = new SerialPort(openedPortName, baudRate, Parity.None, 8, StopBits.One);        // ポートインスタンス作成
        serialPort.Open();                                      // 作成したポートを開く

        isThreadRunning = true;                                 // 実行中フラグ立てる

        readingThread = new Thread(Read);                              // スレッド作成
        readingThread.Start();                                         // スレッド開始(読み込み)

        print("port was setuped.");
    }

    // シリアルポートを閉じる
    void Close()
    {
        // フラグ降ろす
        isNewMessageReceived = false;
        isThreadRunning = false;

        if (readingThread != null && readingThread.IsAlive) {
            // スレッドが終了するまで待機
            print("waiting");

            readingThread.Join(TimeSpan.FromSeconds(.5f));
        }

        // ポートが開いていたら、閉じる
        if (serialPort != null && serialPort.IsOpen) {
            serialPort.Close();         // 閉じる
            serialPort.Dispose();       // リソース開放

            print("port was closed.");
        }
    }

	//--------------------------------------------------

	// シリアルポートに読み込む
	void Read()
    {
        while (isThreadRunning && serialPort != null && serialPort.IsOpen) {

            try {
                // ポートからのバイト数が0より多かったら、読み込み
                if (serialPort.BytesToRead > 0) {
                    message = serialPort.ReadLine();            // データ読み込み
                    isNewMessageReceived = true;                // メッセージ受け取りフラグ立てる
                }
            }

            // 例外
            catch(Exception exception) {
                Debug.LogWarning(exception.Message);
            }
        }
    }

	// シリアルポートに書き込む
	public void Write(string message)
    {
        // 書き込み
        try {
            serialPort.Write(message);
        }

        // 警告
        catch (Exception exception){
            Debug.LogWarning(exception.Message);
        }
    }

    //--------------------------------------------------

    // コンマで区切られたメッセージを返す
    public string[] GetSplitedData()
    {
        // 受け取ったメッセージを区切る
        return message.Split(",");
    }
}

```

</details>

:::note
**Point💡**
1. **`Open()`**
    - M5Stackのシリアルポートをインスタンス化して開きます。
    - また、**データの読み取りを別のスレッドで行うために、読み取り用のスレッドを作成して開始します**。
1. **`Close()`**
    - スレッドを終了して、シリアルポートを閉じます。
1. **`Read()`**
    - 受信されたデータを読み取ります。
    - 別スレッドで処理が実行されます。
1. **`Write()`**
    - データをシリアルポートに送信します。(今回は使用しません。)
1. **`GetSplitedData()`**
    - **コンマ区切りされたデータのメッセージを、コンマで区切って文字列の配列にして返します。**
1. **`OnDataReceived()`**
    - データ受け取り時のイベントです。
    - **`Receiver`クラスで処理が追加されています。(データを読み取り、文字列の配列から値に変換して、フィールドに値を代入しています)**

:::






#### DataReceiver_Base
データ受信用の基底クラスです。
必須ではないです。

<details><summary><b><font color="YellowGreen">📄クリックしてコードを表示</font></summary>

```DataReceiver_Base.cs
using UnityEngine;

// データ受信基底クラス
public abstract class DataReceiver_Base:MonoBehaviour
{
    [SerializeField] protected SerialHandler handler;

	//--------------------------------------------------

	/// <summary>
	/// データ受信時の処理
	/// </summary>
	protected abstract void OnReceivedData();

	//--------------------------------------------------

	private void Awake()
	{
		handler.OnDataReceived += OnReceivedData;
	}
}
```

</details>

:::note
**Point💡**
- `Awake()`で、派生クラスで定義された`OnReceivedData()`を`handler`のイベントに追加します。

:::






#### AccelarationDataReceiver
デバイスの加速度(傾き)を受けとるクラスです。
傾きなんですが、UIFLOWでは加速度と表示されてるので合わせてます

<details><summary><b><font color="YellowGreen">📄クリックしてコードを表示</font></b></summary>

```AccelarationDataReceiver.cs
using UnityEngine;

// デバイスの加速度を取得する
public class AccelarationDataReceiver : DataReceiver_Base
{
	// デバイスの加速度
	public Vector2 Accelaration { get; private set; }       

    //--------------------------------------------------

    // 受信
    protected override void OnReceivedData()
    {
        var data = handler.GetSplitedData();     // データ取得

        try {
            if (data.Length >= 2) {
                // データ文字列からfloat型に変換して、ベクトルに適用
                float x = float.Parse(data[0]);
                float y = float.Parse(data[1]);

                Accelaration = new Vector2(x, y);
            }
        }

        // 例外
        catch (System.Exception exc) {
            Debug.LogWarning(exc.Message);
        }
    }
}
```

</details>

:::note
**Point💡**
- **文字列の配列から、それぞれの要素を`float`型に変換して、それをUnityの`Vector2`型の要素に適用させています。**

:::






#### PlayerController
プレイヤーの回転をさせているクラスです。

<details><summary><b><font color="YellowGreen">📄クリックしてコードを表示</font></b></summary>

```PlayerController.cs
using UnityEngine;

public class PlayerController : MonoBehaviour
{
    [Header("Parameters")]
    [SerializeField, Tooltip("可動範囲")] float rotatableRigion = 90;
    [SerializeField,Tooltip("反転するか")] bool isReverced;

    [Header("Components")]
    [SerializeField] AccelarationDataReceiver receiver;

	//--------------------------------------------------

	private void Awake()
	{
        // 反転させる場合、-1をかける
        if (isReverced) {
            rotatableRigion *= -1;
        }
	}

	// 移動
	void LateUpdate()
    {
        var euler = receiver.Accelaration * rotatableRigion;

        transform.rotation = Quaternion.Euler(euler.y, euler.x, 0);
    }
}

```

</details>

:::note
**Point💡**
- `AccelarationDataReceiver`の加速度の値を適用させて、回転させています。

:::






# ■さいごに
デバイスを利用したゲームを制作するのは、新鮮で楽しいなーって思いました。


## ●参考サイト
- セットアップ方法

https://note.com/kuribata/n/n6c8ebaf4d383

- M5Stackのrx,txなどの指定方法、ボーレートについて

https://lang-ship.com/reference/unofficial/M5StickC/Peripherals/UART/

- コード

https://qiita.com/yjiro0403/items/54e9518b5624c0030531