UnityとM5Stack間をWebSocketで通信して、色を同期させてみた

最近Unity始めてみました。普段は電子工作で遊んでいるので、まだ初心者本を写経しているレベルですが、Unityと他の機器を無線で通信するにはどうすればいいか調べてやってみました。

主にぱっと調べたところUDPやWebSocketあたりが使えそうなのだったので、今回はWebSocketでUnity - M5Stackとの間で通信させてみました。M5StackはESP32というWiFi通信可能なモジュールを搭載して、液晶やボタンが付いたプロトタイピングに便利なガジェットです。


結果はこちらのTweetの通りです。

M5Stackのボタンを赤・緑・青に割り当て、ボタンを押すとUnity上のCubeの色とM5Stackの画面が変化するようにしました。


最終的には複数のM5シリーズ(M5Stack, M5StickC, M5Atom)をUnity起動しているPCと接続して、複数同時に色を同期することができました。



システム構成

構成としては、PC側にUnity Projectがあり、そこにWebSocket ServerとClientをおきます。外部からWiFiで通信するM5SeriesはWebSocket Clientの機能を入れておきます。

画像1

Unity WebSocket-Sharp

UnityでWebSocketを使う場合は、WebSocekt-Sharpというライブラリを使用します。

使い方はこちらにまとめられているのでそちらを参照してください。

まずは上の記事のようにUnity内でWebSocket ServerとClientを用意して、その間で通信できるか確認しました。

ArduinoWebSocket

M5Stack側はArduino IDEを使用しますので、こちらのライブラリを使用しました。

M5Stack側はWebSocket Clientとして使用します。Exampleはこちら。

これらをベースに色を同期させるコードを書きます。


M5Stack (WebSocket Client)

ソースコードです。

#include <WiFi.h>
#include <WebSocketsClient.h>
#include <ArduinoJson.h>
#include <M5Stack.h>
#include <map>
#include "config.h"

WebSocketsClient webSocket;
DynamicJsonDocument doc(1024);

std::map<std::string, uint32_t> colorMap{
   {"red", RED},
   {"green", GREEN},
   {"blue", BLUE}
};

std::string parseReceivedJson(uint8_t *payload)
{
 char *json = (char *)payload;
 DeserializationError error = deserializeJson(doc, json);
 
 if (error) {
   Serial.print(F("deserializeJson() failed: "));
   Serial.println(error.c_str());
   return "none";
 }

 JsonObject obj = doc.as<JsonObject>();

 // You can use a String to get an element of a JsonObject
 // No duplication is done.
 return obj[String("color")];
}

void syncColor(uint8_t *payload)
{
 std::string color = parseReceivedJson(payload);

 Serial.printf("color: %s\n", color.c_str());
 //M5.Lcd.fillRect(60, 20, 200, 200, colorMap[color]);
 M5.Lcd.fillScreen(colorMap[color]);
}

void webSocketEvent(WStype_t type, uint8_t * payload, size_t length) {

	switch(type) {
		case WStype_DISCONNECTED:
			Serial.printf("[WSc] Disconnected!\n");
			break;
		case WStype_CONNECTED:
			Serial.printf("[WSc] Connected to url: %s\n", payload);
			//webSocket.sendTXT("Connected");
			break;
		case WStype_TEXT:
			Serial.printf("[WSc] get text: %s\n", payload);
            syncColor(payload);
			break;
		case WStype_BIN:
		case WStype_ERROR:			
		case WStype_FRAGMENT_TEXT_START:
		case WStype_FRAGMENT_BIN_START:
		case WStype_FRAGMENT:
		case WStype_FRAGMENT_FIN:
			break;
	}
}

void setupWiFi()
{
 WiFi.begin(ssid, passwd);

 // Wait some time to connect to wifi
 for(int i = 0; i < 10 && WiFi.status() != WL_CONNECTED; i++) {
     Serial.print(".");
     delay(1000);
 }

 // Check if connected to wifi
 if(WiFi.status() != WL_CONNECTED) {
     Serial.println("No Wifi!");
     return;
 }

 Serial.println("Connected to Wifi, Connecting to server.");
	// server address, port and URL
	webSocket.begin("192.168.10.11", 8080, "/");

	// event handler
	webSocket.onEvent(webSocketEvent);

	// use HTTP Basic Authorization this is optional remove if not needed
	//webSocket.setAuthorization("user", "Password");

	// try ever 5000 again if connection has failed
	webSocket.setReconnectInterval(5000);
}

void setup()
{
 Serial.begin(115200);
 // Power ON Stabilizing...
 delay(500);
 M5.begin();

 setupWiFi();
 M5.Lcd.fillScreen(BLACK);
 M5.Lcd.setTextColor(GREEN);
 M5.Lcd.setTextSize(2);
}

void loop() {
 bool isPressedBtnA = false;
 bool isPressedBtnB = false;
 bool isPressedBtnC = false;
 if(M5.BtnA.wasPressed() || M5.BtnA.isPressed())
 {
   isPressedBtnA = true;
 }
 if(M5.BtnB.wasPressed() || M5.BtnB.isPressed())
 {
   isPressedBtnB = true;
 }
 if(M5.BtnC.wasPressed() || M5.BtnC.isPressed())
 {
   isPressedBtnC = true;
 }

 static uint32_t pre_send_time = 0;
 uint32_t time = millis();
 if(time - pre_send_time > 100){
   pre_send_time = time;
   String isPressedBtnAStr = (isPressedBtnA ? "true": "false");
   String isPressedBtnBStr = (isPressedBtnB ? "true": "false");
   String isPressedBtnCStr = (isPressedBtnC ? "true": "false");
   String btn_str = "{\"red\":" + isPressedBtnAStr + 
     ", \"green\":" + isPressedBtnBStr + 
     ", \"blue\":" + isPressedBtnCStr + "}";
   //Serial.println(btn_str);
   webSocket.sendTXT(btn_str);
 }
 webSocket.loop();

 M5.update();
}

setupWiFi()はよくあるESP32のWiFi接続処理に加えて、

webSocket.begin()でWebSocektの接続処理を行います。指定しているIP,PORTはPC側の接続先になりますので適宜変更してください。またPC側のFirewall設定やセキュリティソフトの設定によってはPortが開いていないのでその場合は各々の設定必要です。ここでは割愛します。

loop()内でボタン押しを判定し、ボタンが押された場合はその押し判定をtrueとしてJSONを生成して webSocket.sendTXT(btn_str)で送信します。

例えば赤ボタン(左側のボタン)だけ押されるとこのJSONを送信します。

{"red":true, "green":false, "blue":false}

これでServer側にカラーボタンのイベントが送信されます。

Unity (WebSocket Server)

Server側のソースコードです。

using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using WebSocketSharp;
using WebSocketSharp.Net;
using WebSocketSharp.Server;

public class ColorStatusServer: MonoBehaviour {

   WebSocketServer server;
   Queue<string> messages = new Queue<string>();
   Queue<string> colorSetting = new Queue<string>();
   public ColorButtonStatus colorButtonStatus = new ColorButtonStatus();


   [System.Obsolete]
   void Start ()
   {

       server = new WebSocketServer(8080);

       server.AddWebSocketService<ColorStatus>("/", () => new ColorStatus(){
           Messages = messages,
           ColorSetting = colorSetting
       });
       server.Start();

   }

   void Update() {
       if(messages.Count() > 0) {
           string recv_msg = messages.Dequeue();
           //Debug.Log("recv:" + recv_msg);  
           colorButtonStatus = JsonUtility.FromJson<ColorButtonStatus>(recv_msg);

           //Debug.Log("red:" + colorButtonStatus.red);
           //Debug.Log("green:" + colorButtonStatus.green);
           //Debug.Log("blue:" + colorButtonStatus.blue);

	        // MeshRendererのメンバであるマテリアルの色を変更
	        if (colorButtonStatus.red == true) {
               transform.GetChild(0).gameObject.GetComponent<Renderer>().material.color = Color.red;
               colorSetting.Enqueue("red");
	        } else if (colorButtonStatus.green == true) {
               transform.GetChild(0).gameObject.GetComponent<Renderer>().material.color = Color.green;
               colorSetting.Enqueue("green");
	        } else if (colorButtonStatus.blue == true) {
               transform.GetChild(0).gameObject.GetComponent<Renderer>().material.color = Color.blue;
               colorSetting.Enqueue("blue");
	        }
       }
   }

   public void ClearColor()
   {
       transform.GetChild(0).gameObject.GetComponent<Renderer>().material.color = Color.white;
   }

   void OnDestroy()
   {
       server.Stop();
       server = null;
   }

}

public class ColorStatus : WebSocketBehavior
{
   public Queue<string> Messages;
   public Queue<string> ColorSetting;

   public void SendMessage(string msg)
   {
       Sessions.Broadcast(msg);
   }

   protected override void OnMessage (MessageEventArgs e)
   {
       Messages.Enqueue(e.Data);

       ColorButtonStatus buttonStatus = JsonUtility.FromJson<ColorButtonStatus>(e.Data);

       string color = "{\"color\":\"";
       if(ColorSetting.Count > 0)
       {
           color += ColorSetting.Dequeue();
           color += "\"}";
           Sessions.Broadcast(color);
       }

   }

   
}

ColorStatusServer クラスでUnity側のGameObjectの処理を行い、ColorStatus クラスはWebSocektの通信のみを行います。そのクラス間のデータのやりとりは、Queue<string> messagesとQueue<string>colorSettingで行います。

ColorStatusクラスでWebSocket ClientからカラーボタンのJSONがくるとOnMessage()が呼ばれます。そこで

Messages.Enqueue(e.Data);

でColorStatusServer側にデータを渡します、取り出しはUpdate側で行っています。(ここは非同期なのでWebSocektの受信が早すぎるとQueueが溢れます)

ColorStatusServerのUpdate()でRendererを使いCubeの色を変更します。また、M5側にBroadcastするためのcolorSettingを更新します。

ServerはCientに変更する色だけ送ればいいのでJSONはこうなっています。(赤の場合)

{"color":red}

また、Client側のソースをみると、webSocketEvent()というメソッドでServerから受信した色の設定をsyncColor()で設定しています。

void syncColor(uint8_t *payload)
{
 std::string color = parseReceivedJson(payload);

 Serial.printf("color: %s\n", color.c_str());
 //M5.Lcd.fillRect(60, 20, 200, 200, colorMap[color]);
 M5.Lcd.fillScreen(colorMap[color]);
}

M5StackはM5.Lcd.fillScreen()で画面全体の色を設定できるので、これで色を切り替えています。

複数台の接続

コードをみるとわかると思いますが、Client側はServerのみ意識するコードになっているので、どのM5Stackも全く同じコードで起動すればServerと接続できます。(M5StickCとAtomはボタンの扱いが異なるのでそこだけ変更が必要です。)

Server側もClient各々を意識するようにはなっていないので、コードの変更なしにClientが複数接続されても同様に処理することが可能です。今回は色の変更をClientに対してBroadcastしているだけなので良いですが、もしClientごとに個別に制御を変えたい場合は何かしら接続しているClientを判別して処理を切り分ける必要があります。一応WebSocekt-Sharpの機能で接続順は取得できるようでした。(実際に動くかは未確認)

まとめ

より詳細を確認したい場合は、GitHubのコードをみてみてください。

M5側はこちら。

Unity側はこちら。

https://github.com/Katsushun89/color_sync


Unityはちょっと素人すぎて本記事中の言葉が正しくないかもしれないです、すみません。

また、手探りで今回はWebSocekt使ってやってみましたが、Unityと他機器を無線で通信する方法でもっと良いものがあればご意見もらえると助かります。

まだレイテンシーや転送レートがどの程度なのかは未確認なのでそのあたりも確認すると何にどういった使い方ができるかは見込みがたちそうです。

次はUnity側からサーボモーターを動かすのをやってみるのでそれもまた出来たら記事書いてみます!

ここから先は

0字

¥ 100

この記事が気に入ったらチップで応援してみませんか?