###### tags: `ESP32` `Unity`
# Unityで自作コントローラを使いたい!!
## このテーマの執筆理由
 よし、Unityの操作を独自の自作コントローラーで動かしたい!でもUnityとArduinoだとケーブルがうっとしい。
 Bluetooth出来ないだろうか?ESP32というBluetoothを搭載しているArduinoボードがあるではないか!
 でも、ESP32は標準のArduinoIDEで書き込むことはできないのか、といったように意外とめんどくさい。
 本書の目標は、自分で動かしたオブジェクトを保存、再生できるようし、アニメーション機能を使わなくても某任〇堂のゴースト〇ーのようなことが出来るようになることです。そのために以下の内容を順番に説明していきます。
1. ArduinoIDEでESP32マイコンボードの書き込みをできるようにする。
2. ESP32マイコンボードに書き込むプログラム作成
3. Unityの設定
4. Arduino(ESP32)とUnityの通信用のスクリプト作成
5. Unityでオブジェクトのデータをファイルの読み書きを行う
6. ゴーストによせる処理
### 本書の目標
 本書が対象としている読者は、Unityのコントローラに興味を感じている人などです。自作コントローラーって何ぞや?って思われている方でも本書を読むことで気軽に開発を始めることが出来るようになります。UnityやArduinoを少し触れたことある人だとよりスムーズに理解出来ると思いますが、全く初めての方でも出来るように丁寧に説明していきます。
## ESP32の準備
まず、コントローラー作成に必要な物は以下の通りです。
* コントローラーに使うスイッチ、センサ、センサに必要な抵抗及びジャンパワイヤ
* 私はホールセンサーを使いました。ほかにも押しボタンスイッチ、加速度センサー、半固定抵抗などを用いることができます。
* ESP32(http://akizukidenshi.com/catalog/g/gM-11819/)
* Arduinoマイコンの
### Arduino IDEのインストール(Windows)
 最初はArdinoの開発環境から構築していきましょう。ArdinoはArduino IDEという開発環境で開発を行うのがおすすめです。Arduino IDEはこちらのサイトからダウンロード出来ます。(*https://www.arduino.cc/en/Main/Software*)アドレスを打ち込むか検索バーからこのサイトに行くと下の図の画面が出てきます。そして、丸で囲った「Windows ZIP file for non admin install」を選択してzipファイルをダウンロードしましょう。

 その後、下の画面に移動します。ここで丸で囲ってある「JUST DOWNLOAD」を選択するとZipファイルのダウンロードが開始されます。

ダウンロードが完了したらZipファイルを展開しましょう。
展開したフォルダの中にある*arduino\*\*\*.exe*を実行して指示に従ってインストールを進めていきましょう。
**ちなみに、最近ではWindows StoreからArduino IDEをインストールする方法もあります。本章ではそれについては触れませんが興味がある方は調べてみると良いでしょう。**
<!-- ## ESP32へ書き込むための準備 -->
### Arduino IDEでの設定
先ほどの手順でインストールが完了したら、Arduino IDEを起動しましょう。
まず最初は環境設定から行っていきます。
IDEを起動したら、ファイル -> 環境設定を開いてください。

そして、設定画面の下の方にある「追加のボードマネージャのURL」の欄に*https://dl.espressif.com/dl/package_esp32_index.json* を入力し、OKを押しましょう。
<!--
 -->

 次にツール -> ボード -> ボードマネージャと進んでいき、検索ウィンドウに「*esp*」と入力し、*esp32 by Espressif Systems*をインストールしましょう。

 さらにツール -> ボード -> ESP32Arudinoと進んでいき、「ESP32 DevModule」を選択しましょう。

ここまでの作業で環境構築が終わりです。次のセクションからは実際のコントローラープログラムの作成手順について解説していきたいと思います。
<!-- 環境構築 -->
## コントローラーのプログラムを作成する
<!-- 開発工程 -->
ここからはコントローラープログラムの作成手順について説明していきます。
### IOピンからのデータを取得してみる
Arduinoでは様々なセンサーを用いて値を取得することが出来ます。
ここではまずIOピンに流れるデータを取得するプログラムを作成します。
実際にはセンサーから流れるデータをキャッチしますが、ここではセンサーなしでIOピンの値を処理をするプログラムを作成しています。ここで1つ注意することがあります。Arduinoは標準で10bit(1024段階の分解能)ですが、ESP32の取得できるセンサの値の分解能は4096段階となっています。若干違うので十分注意しましょう。
また、利用できるアナログ入力ピンはIO4,13,14,25\~27,32\~36,39で、アナログ出力はIO25,26となっています。
<!-- ここまで校正しました -->
このプログラムはIOピン32、33のそれぞれのデータを取得して過去に取得した値の平均値をとってフィルターをかけるものです。そして、この値を1つの文字列として結合し表示するプログラムです。
```
#include "BluetoothSerial.h"
BluetoothSerial SerialBT;
#define threshold 1600
#define th_val 0.8
int analogdata = 0;
float y[2][2]={0};
char data1[20]={0};
String output,out1,out2;
int len,work;
void setup() {
// put your setup code here, to run once:
Serial.begin(9600);
SerialBT.begin("ESP32");
pinMode(4, OUTPUT);
Serial.print("sensor1,sensor2");
}
void loop() {
out1=String(rc_filter(32));
out2=String(rc_filter(33));
len =out1.length();
while(len<7){
out1 = '0'+out1;
len++;
}
//文字列の長さの統一化
len =out2.length();
while(len<7){
out2 = '0'+out2;
len++;
}me
output=out1+','+out2;
Serial.println(output);
SerialBT.println(output);
delay(10);
}
float rc_filter(int pin)
{
float val_new,val_old;
val_new = y[pin][0];val_old = y[pin][1];
val_new = th_val * val_old + (1.0-th_val)*analogRead(pin);
y[pin][1] = val_new;
return val_new;
}
```
<!-- Todo:プログラムの説明をお願いします -->
### ESP32マイコンボードに書き込む準備
 次に実際のマイコンボードに書き込むための準備をします。まずは、以下のサイトにアクセスしてドライバーをダウンロードしましょう。
*Silicon Laboratories*(*https://jp.silabs.com/products/development-tools/software/usb-to-uart-bridge-vcp-drivers*)から*CP210x_Universal_Windows_Driver*をダウンロードし、zipファイルを解凍します。

解凍したファイルを開き、使用しているWindowsのバージョンに合わせてインストールします。
64ビット版であれば -> \*\*\*x64.exeを選択します。32ビット版であれば -> \*\*\*x86.exe を選択して実行、インストールを行います。

これでドライバ周りの準備が出来ました。このセクションの最後にこれまで紹介したサイトや参考になるサイトを列挙したいと思います。
ドライバURL:https://jp.silabs.com/products/development-tools/software/usb-to-uart-bridge-vcp-drivers
ESP32ボードマネージャのgithub:https://github.com/espressif/arduino-esp32/blob/master/docs/arduino-ide/boards_manager.md
設定画面で貼り付けるURL:https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_index.json
## Unityの準備
ここまでの解説でArduinoの導入方法とポートからのデータ取得方法について解説しました。次はUnity側の準備に入ります。このセクションでは、ArduinoからUnityへデータを渡す方法などを説明しています。
### ArduinoとUnityとの連携
UnityとArduinoの通信はシリアルポート(COMポート)で行います。今回はESP32を用いるのでBluetooth通信とUSBポート通信のどちらかで実装することが出来ます。 なお、今回動作検証を行ったUnityのバージョンは2019.4.2f1です。バージョンによっては正常に動かない可能性があるのでご了承ください。
Unityの初期設定のままではシリアルポート通信ができないので設定で有効化するがあります。まずはメニューバーのEdit -> Project Settings...を開いてください。

次にProject Settingsの中のPlayerを選択します。そして、Api Conpatibility Level* を .NET 4.x に変更します。

### コード
次にUnity側でシリアルポートを読み込むためのスクリプトを作成していきます。
#### SerialHandler.cs
このプログラムは通信COMポートからデータを受け取るプログラムになります。
これについては参考文献のプログラムをそのまま引用した形になります。
```
using UnityEngine;
using System.Collections;
using System.IO.Ports;
using System.Threading;
public class SerialHandler : MonoBehaviour
{
public delegate void SerialDataReceivedEventHandler(string message);
public event SerialDataReceivedEventHandler OnDataReceived;
public string portName = "COM3";
public int baudRate = 9600;
private SerialPort serialPort_;
private Thread thread_;
private bool isRunning_ = false;
private string message_;
private bool isNewMessageReceived_ = false;
void Awake()
{
Open();
}
void Update()
{
if (isNewMessageReceived_)
{
OnDataReceived(message_);
}
}
void OnDestroy()
{
Close();
}
private void Open()
{
serialPort_ = new SerialPort(portName, baudRate, Parity.None, 8, StopBits.One);
serialPort_.Open();
isRunning_ = true;
thread_ = new Thread(Read);
thread_.Start();
}
private void Close()
{
isRunning_ = false;
if (thread_ != null && thread_.IsAlive)
{
thread_.Join();
}
if (serialPort_ != null && serialPort_.IsOpen)
{
serialPort_.Close();
serialPort_.Dispose();
}
}
private void Read()
{
while (isRunning_ && serialPort_ != null && serialPort_.IsOpen)
{
try
{
// if (serialPort_.BytesToRead > 0) {
message_ = serialPort_.ReadLine();
isNewMessageReceived_ = true;
// }
}
catch (System.Exception e)
{
Debug.LogWarning(e.Message);
}
}
}
public void Write(string message)
{
try
{
serialPort_.Write(message);
}
catch (System.Exception e)
{
Debug.LogWarning(e.Message);
}
}
}
```
### serial.cs
このプログラムはArduinoから送信したデータを取得するプログラムのコア部分の実装になります。ここで実際には様々処理をしますが、今回はデバッグログの出力だけになっています。
<!-- シリアルポートからデータを読み取る基本の形 -->
```
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Serial : MonoBehaviour
{
public SerialHandler serialHandler;
void Start()
{
serialHandler.OnDataReceived += OnDataReceived;
}
void OnDataReceived(string message)
{
//コールルーチン呼び出し
var data = message.Split(new string[] { "\n" }, System.StringSplitOptions.None);
//改行で受け取ったメッセージの分割を行う
Debug/Log(data);
if (data.Length != 1) return;
}
}
```
### SplitSeparate.cs
さらに上のプログラムをを改良しESP32側で想定しているデータ2つを受け取り、ログに出力するプログラムになります。
```
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System;
public class SplitSeparate : MonoBehaviour {
public float[] analogdata= { 0, 0 }; //初期化
public SerialHandler serialHandler;
private int i;
void Start()
{
serialHandler.OnDataReceived += OnDataReceived;
}
void OnDataReceived(string message)
{
var data = message.Split(new string[] { "\n" }, System.StringSplitOptions.None);
if (data.Length != 1) return;
string[] arr = data[0].Split(',');
//「,」で分割して統合されているデータ
for (i = 0; i < 2; i++)
analogdata[i] = float.Parse(arr[i]);
Debug.Log("data1:"+analogdata[0]);
Debug.Log("data2:"+analogdata[1]);
}
}
```
このスクリプトを操作するオブジェクトにドラッグ&ドロップし適応させましょう。
*SplitSaparate.cs*を適応させた場合、*SerialHandler*が*None*になっているので矢印のようにドラッグ&ドロップします。
すると、\[オブジェクト名\](SerialHandler)となるはずです。
ここまでできればデータを受け取れたので、ボタンと座標を連動すれば自作コントローラで操作できるようになるはずです。

これにより、自作コントローラの入力をオブジェクトに適応させることが可能となりました。
次は動かしたオブジェクトの座標データや座標とは別のステータスをセンサで管理しているデータをCSVファイルで保存するためのスクリプトを作成します。スクリプトが呼び出されたタイミングから時間を計り、時間ごとに座標等のデータを保存します。これにより動きが保存できます。
### csvOutput.cs
シリアルポートから受け取ったデータやスクリプトを適応しているオブジェクトの座標をcsvファイル形式で保存するスクリプトです。
CSVファイルが大量に生成されるため、一か所にまとめて見やすくするためにフォルダを作成します。
AssetsにResoucesフォルダを作り、その中にcsvfileというフォルダを作ったものになります。
```
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.Text;
using System.IO;
public class csvOutput : MonoBehaviour
{
public SerialHandler serialHandler;
//float analogdata = 0;
private float time,beforeTime;
string filename = DateTime.Now.ToLongTimeString();
StreamWriter sw;
Transform myTransform;
Vector3 mypos;
Quaternion myquart;
//出力する場所のパスの指定と、ファイル名の指定
string nowTime = "./Assets/Resources/csvfile/" + System.DateTime.Now.ToString("yyyy_MM_dd_hh_mm_ss") + ".csv";
void Start()
{
time = 0.0f;
beforeTime = -0.1f;
// ファイル書き出し
// 現在のフォルダにsaveData.csvを出力する(決まった場所に出力したい場合は絶対パスを指定してください)
// 引数説明:第1引数→ファイル出力先, 第2引数→ファイルに追記(true)or上書き(false), 第3引数→エンコード
sw = new StreamWriter(@nowTime, false, Encoding.GetEncoding("Shift_JIS"));
// ヘッダー出力
string[] s1 = {"i" , "Time", "Value1","Value2", "x_pos", "y_pos", "z_pos", "x_rot", "y_rot", "z_rot", "w_rot" };
//行数、時間センサの値1、時間センサの値2、x座標、y座標、z座標、x軸回転、y軸回転、z軸回転、wは基本いじらない
string s2 = string.Join(",", s1);
sw.WriteLine(s2);
/*
// ファイル読み込み
// 引数説明:第1引数→ファイル読込先, 第2引数→エンコード
StreamReader sr = new StreamReader(@"saveData.csv", Encoding.GetEncoding("Shift_JIS"));
string line;
// 行がnullじゃない間(つまり次の行がある場合は)、処理をする
while ((line = sr.ReadLine()) != null)
{
// コンソールに出力
Debug.Log(line);
}
// StreamReaderを閉じる
sr.Close();*/
// コルーチンを設定
StartCoroutine(loop());
}
void Update() {
time += Time.deltaTime;
}
private IEnumerator loop()
{
// ループ
while (time != beforeTime)
{
// 0.01秒毎にループします
yield return new WaitForSeconds(0.01f);
serialHandler.OnDataReceived += OnDataReceived;
}
}
void OnDataReceived(string message)
{
beforeTime = time;
myTransform = this.gameObject.transform;
mypos = myTransform.position;
myquart = myTransform.rotation;
var data = message.Split(new string[] { "\n" }, System.StringSplitOptions.None);
if (data.Length != 1) return;
Debug.Log("time:" + time);
//時間の確認
myTransform = this.gameObject.transform;
mypos = myTransform.position;
//座標更新
myquart = myTransform.rotation;
//回転座標更新
float lastTime = 0.0f;
string[] str = { "" + time + "," + data[0] + "," + mypos.x + "," + mypos.y + "," + mypos.z + "," + myquart.x + "," + myquart.y + "," + myquart.z + "," + myquart.w };
string str2 = string.Join(",", str);
//Arduinoから受け取ったデータの分割
if(lastTime != time)
sw.WriteLine(str2);
lastTime = time;
}
private void OnApplicationQuit()
{
sw.Close();
}
}
```
**注意点:現状のこのスクリプトではスクリプトが呼び出されるたびにcsvファイルが生成されます。**
ファイルに保存ができたら次は読む必要がある。
次のスクリプトは読みだしたデータをスクリプトが呼び出されたタイミングから時間を計り、
その時間に応じた動きを再生するプログラムになる。
### Ghost.cs
上記で保存したcsvファイルを用いて動きを再現するスクリプト。
再生したいオブジェクトに適応させ、
再生したいcsvファイルを先ほどと同様にドラッグ&ドロップします。
```
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.IO;
public class Ghost : MonoBehaviour
{
List<string[]> csvDatas = new List<string[]>();
public string fname = "hoge.csv";
Transform myTransform;
Vector3 mypos;
Quaternion myquart;
private float time;
int i = 1;
float pbTime = 0.0f;
// Use this for initialization
void Awake()
{
myTransform = this.gameObject.transform;
mypos = myTransform.position; //座標
myquart = myTransform.rotation; //角度
string path = "csvfile/" + fname; //ファイルへのアクセス、相対パス
var csv = Resources.Load(path) as TextAsset;
var reader = new StringReader(csv.text);
while (reader.Peek() > -1)
{
// ','ごとに区切って配列へ格納
string line = reader.ReadLine();
csvDatas.Add(line.Split(','));
}//ファイルのデータを配列に格納
time = 0.0f - Time.time;
}
// Update is called once per frame
void Update()
{
Transform t = this.gameObject.GetComponent<Transform>();
time += Time.deltaTime;
pbTime = float.Parse(csvDatas[i][0]);
if (Mathf.Abs(pbTime - time) < 0.01f)
{
//スクリプトを開始してからの時間と読み込んだ時間の差が0.01秒以内の時に座標を更新する。
t.position = new Vector3(float.Parse(csvDatas[i][3]), float.Parse(csvDatas[i][4]), float.Parse(csvDatas[i][5]));
t.rotation = new Quaternion(float.Parse(csvDatas[i][6]), float.Parse(csvDatas[i][7]), float.Parse(csvDatas[i][8]), float.Parse(csvDatas[i][9]));
i += 5; //再生時の負荷のほうが大きいため、1ずつ増やすと遅くなるため、少し飛ばしながら再生
}
else if (pbTime > time)
{
//早い場合何もしない
}
else
{
i += 25; //再生が遅れている場合、飛ばす
}
}
}
```
## オブジェクトの半透明化
<!-- Colorのアルファ値を操作して透明度を調整する。 -->
次に、Colorのα値を操作して透明度の調整を行います。
これによって自分が動かしているオブジェクトや
当たり判定のあるオブジェクトなどと見分けやすくなります。
参考URL:*https://nn-hokuson.hatenablog.com/?page=1517310050*
Assetsで右クリックをし、Create -> Shader -> Standard Surface Shaderを選択します。
次に作成したShaderファイルを右クリックし、Create -> Materialを選択します。
最後にShaderファイルを開き、下記プログラムを記述する。
### semiTransparent.shader
```
Shader "SemiTransparent" {
Properties{
_Color("Color", Color) = (1,1,1,1)
_MainTex("Albedo (RGB)", 2D) = "white" {}
}
SubShader{
Tags { "RenderType" = "Transparent" "Queue" = "Transparent"}
LOD 200
Pass{
ZWrite ON
ColorMask 0
}
CGPROGRAM
#pragma surface surf Standard fullforwardshadows alpha:fade
#pragma target 3.0
sampler2D _MainTex;
struct Input {
float2 uv_MainTex;
};
fixed4 _Color;
void surf(Input IN, inout SurfaceOutputStandard o)
{
fixed4 c = tex2D(_MainTex, IN.uv_MainTex) * _Color;
o.Albedo = c.rgb;
o.Metallic = 0;
o.Smoothness = 0;
o.Alpha = c.a;
}
ENDCG
}
FallBack "Diffuse"
}
```

ここまでで自作コントローラでオブジェクトを動かし、そのオブジェクトのゴーストにより、動きを比較できるようになりました。
今後は、これらを用いてレースゲームの最速記録と勝負したり、自分独自の新しいコントローラの作成ができるだろう。
## あとがき
ここまでUnityの小難しい話を交えつつコントローラーの作成について解説をしてきました。Unityには使い道はあるけど地味にめんどくさいテクニックがたくさんあるので最初は難しいと思われるかもしれません。また、特にArduinoとファイル出力は割と簡単ですが、それらをまとめて扱うとなるとハードルが少し上がります。さらに、コールルーチン等について理解してなければセットでかくには分かりにくいところが多いため、少し前の私と同じように困っている人の助けになればうれしく思います。
最後になりますが、書き物に慣れてなく読みにくい読んでいて面白くない等あったと思いますが、最後まで読んでいただきありがとうございました。