在上一篇文章Unity 串列埠連接Arduino:測試(二),已經將Arduino與Unity的專案連接成功,可以透過Arduino端的搖桿來控制Unity中的物件移動與旋轉。接著在本篇文章中我們兩個目標要達成:
(1)將UI移到一個新場景中,利用DontDestroyOnLoad的功能,讓Unity即使在場景變更中,串列埠的設定介面仍然存在、連線不會中斷。
(2)Unity端不再採用FixedUpdate,改成利用Thread來進行串列埠資料讀取。
首先在Unity中新增一個場景UI_0,場景中只放一個空物件,將UI(包括Canvas和EventSystem)移到這空物件下。並在Canvas下新增三個Button:
對應的函數如下:
using UnityEngine.SceneManagement;
public void S1()
{
SceneManager.LoadScene("Scene1");
}
public void S2()
{
SceneManager.LoadScene("Scene2");
}
public void GameQuit()
{
Application.Quit();
}
與第一篇方式相同,將對應的事件與執行的函數設定好。
接著將第一個場景改名為Scene1,並新增一個場景Scene2,在Scene2放入一個3D物件。
新增一個腳本S2motio.cs,附加在該物件上:
------------------------------------------------------------
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class S2motion : MonoBehaviour
{
public float rspeed = 30f;
void Start()
{
}
// Update is called once per frame
void Update()
{
transform.position = new Vector3((-ComData.X + 512) / 10 * 0.1f, 0, (ComData.Y - 512) / 10 * 0.1f);
if (ComData.R == 0)
{
transform.Rotate(Vector3.forward * Time.deltaTime * rspeed);
}
}
}
----------------------------------------------------------------------------------------
接著新增一個腳本DDO.cs, 附加在UI_0場景中的GameObject(底下有Canvas和EventSystem等)
-------------------------------------------------------------------------------------
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;
public class DDO : MonoBehaviour
{
// Start is called before the first frame update
void Start()
{
DontDestroyOnLoad(gameObject);
SceneManager.LoadScene("Scene1");
}
// Update is called once per frame
void Update()
{
}
}
-------------------------------------------------------------------------------
該腳本執行時,會將物件移到DontDestroyOnLoad,並且載入Scene1。
接著將Serialsetup.cs修改如下:
--------------------------------------------------------------------
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using System.IO.Ports;
using System;
using UnityEngine.SceneManagement;
using System.Threading;
public class Serialsetup : MonoBehaviour
{
public static SerialPort sp = new SerialPort();
private string V = "1";
private string[] words = null;
public GameObject Canv, B_C, B_D;
public Dropdown Dp1;
private string PN = null;
private string[] Com_name=null;
private List<string> list;
private bool b_conv = true;
private Thread t;
private Boolean receiving;
public void GameQuit()
{
Application.Quit();
}
public void S1()
{
SceneManager.LoadScene("Scene1");
}
public void S2()
{
SceneManager.LoadScene("Scene2");
}
public void AddSerial()
{
PN = Dp1.options[Dp1.value].text;
if (!sp.IsOpen)
{
B_C.SetActive(true);
B_D.SetActive(false);
}
}
public void Connect()
{
try
{
sp.BaudRate = 115200;
sp.Parity = Parity.None;
sp.DataBits = 8;
sp.StopBits = StopBits.One;
sp.PortName = PN;
sp.ReadTimeout = 2;
B_D.SetActive(true);
B_C.SetActive(false);
if (!sp.IsOpen)
{
sp.Open();
receiving = true;
t = new Thread(DoReceive);
t.IsBackground = true;
t.Start();
}
}
catch (System.Exception)
{
}
}
private void DoReceive()
{
while (receiving)
{
try
{
if (sp.BytesToRead > 0)
{
ReadandSplit();
}
}catch (System.Exception)
{
Debug.Log("not good");
}
Thread.Sleep(5);
}
}
public void Disconnect()
{
receiving = false;
t.Abort();
sp.Close();
B_C.SetActive(true);
B_D.SetActive(false);
}
void Start()
{
B_C.SetActive(false);
B_D.SetActive(false);
Com_name = SerialPort.GetPortNames();
list= new List<string>(Com_name);
Dp1.AddOptions(list);
}
// Update is called once per frame
void Update()
{
if (sp.IsOpen)
{
if (Input.GetKeyDown(KeyCode.Escape))
{
b_conv = !b_conv;
Canv.SetActive(b_conv);
}
}
else
{
Canv.SetActive(true);
}
}
private void ReadandSplit()
{
for (int i=1; i<5; i++)
{
V = sp.ReadLine();
while (sp.BytesToRead > 30)
{
V = sp.ReadLine();
Debug.Log("R");
}
words =V.Split(',');
if (V.StartsWith("B") && V.EndsWith("D"))
{
ComData.R = Int32.Parse(words[1]);
ComData.X = Int32.Parse(words[2]);
ComData.Y = Int32.Parse(words[3]);
Debug.Log(i);
break;
}
}
}
void OnDisable()
{
t.Abort();
sp.Close();
}
void OnApplicationQuit()
{
t.Abort();
sp.Close();
}
}
----------------------------------------------------------------------------------
主要是Connect的修改,其新增一個Thread,並以DoReceive來讀取資料。
public void Connect()
{
try
{
sp.BaudRate = 115200;
sp.Parity = Parity.None;
sp.DataBits = 8;
sp.StopBits = StopBits.One;
sp.PortName = PN;
sp.ReadTimeout = 2;
B_D.SetActive(true);
B_C.SetActive(false);
if (!sp.IsOpen)
{
sp.Open();
receiving = true;
t = new Thread(DoReceive);
t.IsBackground = true;
t.Start();
}
}
catch (System.Exception)
{
}
}
private void DoReceive()
{
while (receiving)
{
try
{
if (sp.BytesToRead > 0)
{
ReadandSplit();
}
}catch (System.Exception)
{
Debug.Log("not good");
}
Thread.Sleep(5);
}
}
-------------------------------------------------------------
在DoReceive()中,BytesToRead > 0即進行資料讀取(ReadLine)與拆解。
在ReadandSplit(),也做了一些修正:
-----------------------------------------------------
private void ReadandSplit()
{
for (int i=1; i<5; i++)
{
V = sp.ReadLine();
while (sp.BytesToRead > 15)
{
V = sp.ReadLine();
Debug.Log("R");
}
words =V.Split(',');
if (V.StartsWith("B") && V.EndsWith("D"))
{
ComData.R = Int32.Parse(words[1]);
ComData.X = Int32.Parse(words[2]);
ComData.Y = Int32.Parse(words[3]);
Debug.Log(i);
break;
}
}
}
---------------------------------------------------
之前我們使用DiscardInBuffer,在丟棄資料時,可能會造成一行資料被丟棄一部分,產生錯誤。
另外,利用Split拆解字串,拆解後檢查第一個字串(words[0])和第五個字串(words[4]),這個方法有疑慮,因為當資料不正確時,words[4]可能不存在。
針對第一個問題,我們改成先讀取一次(ReadLine),接著如果BytesToRead大於某個數字(依資料量而定),則再繼續讀取,這樣可以達到丟棄資料的效果,也不會破壞資料整行的完整性。
對於第二個問題,則改用StartsWith("B") 和EndsWith("D")來檢查資料的正確性。
在Disconnect()也需加入
receiving = false;
t.Abort();
最後再加入
void OnDisable()
{
t.Abort();
sp.Close();
}
void OnApplicationQuit()
{
t.Abort();
sp.Close();
}
當應用程式關閉時,關閉這個Thread和串列埠連線。
測試時,Arduino端的dealy 1ms,Unity端的Thread.Sleep 5ms。
執行結果如下:
讀取資料成功50165行時,丟棄資料124029次,大約2:5。沒有讀第二次才成功的狀況產生,not good的狀況也只產生一次。場景切換也成功。
Build時產生一個警告訊息
System.Windows.Forms.dll assembly is referenced by user code, but is not supported on StandaloneWindows64 platform. Various failures might follow.
UnityEngine.GUIUtility:ProcessEvent (int,intptr,bool&)
但執行Build出來的執行檔,可執行且測試成功。
上面這個警告訊息,經過追蹤,似乎在Api Compatibility Level設定為.Net 4.x之後就出現了!