memorandums

日々の生活で問題解決したこと、知ってよかったことなどを自分が思い出すために記録しています。

Unityの複数シーンでシリアル通信を共有したかった話

背景

いよいよ明後日に迫ったMFK2024。当ゼミの3年生2名が出展を決めて最後の追い上げをゼミ室でやっていました。

Unity担当の学生さんがトップ、メイン、リザルトと複数シーンにわけたときに、Arduino側と通信するプログラムがうまくシーン間で共有できない、ということで悩んでいました。

私も経験がなかったので帰宅してから色々と調べてみました。

とりあえず動いた方法

とりあえず、シリアル通信はstatic classにします。そして、そこからデータを取り出すためのクラスもstaticにします。シーン間でグローバル変数的な単一のデータ記憶領域を確保することができることがわかりました。

具体的には以下です。

SerialHandler.cs

using UnityEngine;
using System.Collections;
using System.IO.Ports;
using System.Threading;

public static class SerialHandler 
{
    private static string portName = "/dev/cu.wchusbserial10"; //★
    private static int baudRate    = 9600; //★
    private static SerialPort serialPort_;
//    private static Thread thread_;
//    private static bool isRunning_ = false;
    public static void Open()
    {
        serialPort_ = new SerialPort(portName, baudRate, Parity.None, 8, StopBits.One);
        serialPort_.Open();

//        isRunning_ = true;

//        thread_ = new Thread(Read);
//        thread_.Start();
    }

    public static void Close()
    {
//        isRunning_ = false;

//        if (thread_ != null && thread_.IsAlive) {
//            thread_.Join();
//        }

        if (serialPort_ != null && serialPort_.IsOpen) {
            serialPort_.Close();
            serialPort_.Dispose();
        }
    }

    public static void Read()
    {
//        while (/*isRunning_ &&*/ serialPort_ != null && serialPort_.IsOpen) {
    if (serialPort_ != null && serialPort_.IsOpen) {
            try {
                string message = serialPort_.ReadLine();
                //============================ここから★
                switch(message[0]) {
                    case '1':
                        GameData.on_b1();
                        break;
                    case '2':
                        GameData.on_b2();
                        break;
                }
                //============================ここまで★
            } catch (System.Exception e) {
                Debug.LogWarning(e.Message);
            }
        }
    }

    public static void Write(string message)
    {
        try {
            serialPort_.Write(message);
        } catch (System.Exception e) {
            Debug.LogWarning(e.Message);
        }
    }
}

GameData.cs

public static class GameData {
    private static bool b1 = false;
    private static bool b2 = false;

    public static void on_b1() {
        b1 = true;
    }
    public static void on_b2() {
        b2 = true;
    }
    public static bool get_b1() {
        bool tmp = b1;
        b1 = false;
        return tmp;
    }
    public static bool get_b2() {
        bool tmp = b2;
        b2 = false;
        return tmp;
    }
}

さらに、各シーンにテスト用の以下のスクリプトを配置します。とりあえずテスト用に2つシーンを作り、そのシーンにあるMain Cameraに以下のスクリプトをそれぞれアタッチします。

S1とS2は内容的にほとんど同じです。シーンの切り替え先がお互いを指しているだけです。

S1.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;

public class S1 : MonoBehaviour
{
    void Awake() {
        SerialHandler.Open();
    }

    void OnDestroy() {
        SerialHandler.Close();
    }

    // Start is called before the first frame update
    void Start()
    {
        
    }

    // Update is called once per frame
    void Update()
    {
        SerialHandler.Read();

        if (Input.GetKey(KeyCode.Space)) {
            SceneManager.LoadScene("S2");
        }
        if (GameData.get_b1()) {
            Debug.Log("b1 was pushed");
        }
        if (GameData.get_b2()) {
            Debug.Log("b2 was pushed");
        }
    }
}

S2.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;

public class S2 : MonoBehaviour
{
    void Awake() {
        SerialHandler.Open();
    }

    void OnDestroy() {
        SerialHandler.Close();
    }

    // Start is called before the first frame update
    void Start()
    {
        
    }

    // Update is called once per frame
    void Update()
    {
        SerialHandler.Read();

        if (Input.GetKey(KeyCode.Space)) {
            SceneManager.LoadScene("S1");
        }
        if (GameData.get_b1()) {
            Debug.Log("b1 was pushed");
        }
        if (GameData.get_b2()) {
            Debug.Log("b2 was pushed");
        }
    }
}

ちょっともう夜も遅いので詳しくは書けませんが、2つのシーンをスペースキーを押すことで切り替える感じなんですが。

SerialHandlerはシングルトンのようなものなのでメモリ上はシーンに関係なく存在し続けるという理解なんですね。。。どちらのシーンからでもアクセスできることはテストで確かめたので。

なので、自分の理解では最初に開くシーンのAwakeでOpenを実行すれば、もう1つのシーンのAwakeでOpenをする必要なんてないはずなんですけど。。。これをやらないとうまくいかないんです。これが不思議。本当はSerialHandlerでThread処理してwhileループで受信待ちするようにしたかったのですが(元々がそうなっていた)、この理由でシーンが裏側に回ったときにシリアルの受信が止まってしまう状況がありました。原因というか仕組みはわかっていません。

とりあえず、このやり方で動いたのでまずはよしとするしかないかな。。。と思っています。

以下、テキストだけでは説明が難しいので夜中ですがUnity Editorを見ながら解説してみました。必要な方は参考にしてください。仕組みや解決法などわかりましたら教えてくださいませ。

あ、ついでにテスト用のArduinoのコードも挙げておきます。

void setup() {
  // put your setup code here, to run once:
  Serial.begin(9600);
  pinMode(13, OUTPUT);
}

void loop() {
  Serial.println("1");
  digitalWrite(13, HIGH);
  delay(500);
  Serial.println("2");
  digitalWrite(13, LOW);
  delay(500);
  // put your main code here, to run repeatedly:

}

youtu.be