2019年12月3日 星期二

Unity內使用JSON的方式

JSON已是經典的資料格式之一了,而以前我在物聯網公司工作時,其中一項工作就是處理那一大堆的JSON資料。

那實在是整死人,因為那時的資料架構很多是沒有固定的,也就是裡面的欄位會任意增減,所以我都用原始的方式去一一檢驗裡面的架構再取得資料。

現在就輕鬆多了,資料架構都是事先固定的,故可用JSONObject一次性搞定,這邊就來記錄一下。

這邊使用的是LitJson,其他的Json也有一樣的做法。

首先是準備資料,我準備了一個JSON字串如下:
[{"name":"這是一號清單","listID":1,"listData":[{"id":1,"content":"一號內容1"},{"id":2,"content":"一號內容2"},{"id":3,"content":"一號內容3"}]},{"name":"這是二號清單","listID":2,"listData":[{"id":1,"content":"二號內容1"},{"id":2,"content":"二號內容2"},{"id":3,"content":"二號內容3"},{"id":4,"content":"二號內容4"},{"id":5,"content":"二號內容5"}]}]

使用網路上的JSON Reader可看到排版後的資料內容:

這是一個兩層Array的架構,可以有數份清單,每份清單內有數個被編號的內容。

接著當要在程式內把這字串寫出來時,需要變換符號,主要是『"』這符號必須要變更,變更方式等在網路上都找得到,這裡就不贅述。
[{\"name\":\"這是一號清單\", \"listID\":1, \"listData\":[{\"id\":1, \"content\":\"一號內容1\"},{\"id\":2,\"content\":\"一號內容2\"},{\"id\":3,\"content\":\"一號內容3\"}]},{\"name\":\"這是二號清單\",\"listID\":2,\"listData\":[{\"id\":1,\"content\":\"二號內容1\"},{\"id\":2,\"content\":\"二號內容2\"},{\"id\":3,\"content\":\"二號內容3\"},{\"id\":4,\"content\":\"二號內容4\"},{\"id\":5,\"content\":\"二號內容5\"}]}]

然後就來正式寫程式吧:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using LitJson;

public class DataController : MonoBehaviour
{
    public class DataClass
    {
        public string name;
        public int listID;
        public ListDataClass[] listData;
    }

    public class ListDataClass
    {
        public int id;
        public string content;
    }

    private string jsonContent = "[{\"name\":\"這是一號清單\", \"listID\":1, \"listData\":[{\"id\":1, \"content\":\"一號內容1\"},{\"id\":2,\"content\":\"一號內容2\"},{\"id\":3,\"content\":\"一號內容3\"}]},{\"name\":\"這是二號清單\",\"listID\":2,\"listData\":[{\"id\":1,\"content\":\"二號內容1\"},{\"id\":2,\"content\":\"二號內容2\"},{\"id\":3,\"content\":\"二號內容3\"},{\"id\":4,\"content\":\"二號內容4\"},{\"id\":5,\"content\":\"二號內容5\"}]}]";
    private JsonData jsonData;
    private List dataClassList = new List();
    private DataClass dataClass = new DataClass();

    void Start()
    {
        jsonData = JsonMapper.ToObject(jsonContent);
        Debug.Log("這是直接版的【Name】:" + jsonData[0]["name"]);
        Debug.Log("這是直接版的【一號清單內的第3項之Content】:" + jsonData[0]["listData"][2]["content"]);
        Debug.Log("------------------------------------------------------------------");

        dataClassList = JsonMapper.ToObject>(jsonContent);

        for (int i = 0; i < dataClassList.Count; i++)
        {
            dataClass = dataClassList[i];

            //Debug.Log("【" + i + "=Name" + "】:" + dataClass.name);
            //Debug.Log("【" + i + "=List ID" + "】:" + dataClass.listID);

            Debug.Log(i);
            Debug.Log("     name:" + dataClass.name);
            Debug.Log("     listID:" + dataClass.listID);
            Debug.Log("     listData");

            for (int j = 0; j < dataClass.listData.Length; j++)
            {
                //Debug.Log("【" + i + "-" + j + "=ID" + "】:" + dataClass.listData[j].id);
                //Debug.Log("【" + i + "-" + j + "=Content" + "】:" + dataClass.listData[j].content);

                Debug.Log("         " + j);
                Debug.Log("             id:" + dataClass.listData[j].id);
                Debug.Log("             content:" + dataClass.listData[j].content);
            }
        }
    }

    void Update()
    {
        if (Input.GetKeyDown(KeyCode.Space))
        {
            dataClassList[0].name = "重新取名字";

            jsonContent = JsonMapper.ToJson(dataClassList);
        }
    }
}

第8行和第15行的兩個Class,是依照JSON的兩層Array架構去宣告的,Class名稱可以任意,但是裡面的變數名稱變數類型一定要完全和JSON的資料一樣。

第28行到第30行是展示用原本的原始方式來取資料。

第33行開始是把JSON字串轉成JSONObject,並套到之前宣告的資料模型內,這樣便可以隨心所欲地提取資料了。

第61行是展示用這種資料模型方式來儲存修改的資料,之後便可依照各自需求把修改後的JSON字串發出去。

程式裡我有弄兩種方式顯示資料,這邊只顯示其中一種,執行程式後在Unity的Console可以看得到:

在使用JSON時最麻煩的是資料被包了又包、包了又包,動輒就好幾層;因此使用這種資料模型的方式時也得去宣告相對應的Class,反而實作只需一行便可取得資料了。

2019年12月2日 星期一

Unity內C#的Event使用方式

C語言有很多實作功能我在過往寫專案時都不會使用到,就像你有一台手機但大部分的附屬功能平常不需要用到一樣,因此自然就沒也特別去注意。但以後也許會有需要用到,還是先研究一下做個記錄比較好。

會用Event的情況通常是我在和其他SDK等做整合時,都已經有那些SDK準備好的Event可直接註冊使用,因此不需要了解那些Event是怎麼生成的;而這邊記錄的是完全無中生有,包含Event的生成、註冊和使用,這樣以後就可以在自己的程式自己來了。

以下是簡單的創造一個Event並來使用:

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

public class ZZZ : MonoBehaviour
{
    class TypeEventArgs : EventArgs
    {
        public float x = 0;
        public float y = 0;
    }

    private event EventHandler eventHandler;
    private TypeEventArgs tempEventArgs = new TypeEventArgs();

    void Start()
    {
        this.eventHandler += ReceiveData;
    }

    void Update()
    {
        tempEventArgs.x += Time.deltaTime;
        tempEventArgs.y += 2 * Time.deltaTime;

        eventHandler(this, tempEventArgs);
    }

    public void ReceiveData(System.Object sender, EventArgs e)
    {
        Debug.Log(Mathf.FloorToInt((e as TypeEventArgs).x) + " = " + Mathf.FloorToInt((e as TypeEventArgs).y));
    }
}

我做了一個Event,然後觸發此Event的情況是每個Frame時,會顯示一個累加時間和其兩倍的時間值。因此觸發條件便可因應各種需要,例如點擊一個按鈕時、用USB連接的Arduino傳過來資料時、或是有預料之中的錯誤發生時等。就可用Event的形式來加以對應。

2019年9月24日 星期二

Unity內偵測外部資料夾內的檔案變化

有很多案子是需要為使用者留下記錄、和二次使用該記錄等,因此像是當拍下使用者的照片、為使用者製作的卡片、或是為使用者錄製的音檔等,會放置在指定的資料夾內,而其他程式會偵測那些指定資料夾內是否有新檔案出現,有的話便會拿來使用,達成一種更新的狀態。

以前我是會很笨地用List記住資料夾內的檔案,然後每幾秒便用For迴圈去比對檢查,現在有空去研究,便發現了王道方法,輕鬆且簡單。

C#有FileSystemWatcher這個Library,專門用來監視資料夾檔案狀態,因此簡單地設定和啟用,便可為程式去監聽指定的資料夾內檔案狀態。

using System.Collections;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.IO;

public class XXX : MonoBehaviour {

    private FileSystemWatcher fileSystemWatcher;

    void Start ()
    {
        DetectFileAction();
    }
 
    void Update ()
    {
  
 }

    public void DetectFileAction()
    {
        fileSystemWatcher = new FileSystemWatcher("S:/Data/", "*.txt");

        fileSystemWatcher.Created += OnChanged;
        fileSystemWatcher.Renamed += OnChanged;
        fileSystemWatcher.Changed += OnChanged;
        fileSystemWatcher.Deleted += OnChanged;
        
        fileSystemWatcher.EnableRaisingEvents = true;
    }
    
    void OnChanged(object source, FileSystemEventArgs e)
    {
        Debug.Log("有檔案被" + e.ChangeType + " = " + e.FullPath);
    }
}

可以看到我偵測四種狀態:創造檔案、重新命名檔案、改變檔案的內容和刪除檔案。

fileSystemWatcher.EnableRaisingEvents是指啟動此程序的意思。

由上而下分別是創造檔案、重新命名檔案、改變檔案的內容和刪除檔案時所出現的相對應訊息,這邊特地列出是為了表明有些時候做某個動作,所得到的回饋會不只一個。

最後有一個很重要的是,如果是較大的檔案,最好不要接收到訊息便立即拿來使用。例如我用Web攝影機錄製了一段使用者的影像,並且將影片儲存到某個資料夾內;雖然其他的程式會立刻接收到Created的訊息,但是檔案實際上可能還沒有創建完成,所以若立即取得並播放的話很可能是白色畫面,因此我都會等一兩秒後再來處理。

2018年5月11日 星期五

Unity內將檔案上傳到FTP

在做專案時有時會需要將檔案上傳到FTP去,像是照片或影片等,所以這邊便記錄一下做法。

我分別遇過需上傳到一般FTP、SFTP和使用主動模式FTP的情況,因此實做過三種方式:FTP的兩種,SFTP和使用主動模式FTP的一種。

這邊只記錄FTP的兩種,第三種有點麻煩,之後有空整理再另外放上來。

使用WebClient的第一種:
using System.Collections;
using System.Collections.Generic;
using System.Net;
using System.Threading;
using UnityEngine;

public class XXX : MonoBehaviour
{
    void Start()
    {
        StartCoroutine(FTPUpload1());
    }
    
    void Update()
    {

    }
    
    IEnumerator FTPUpload1()
    {
        var filename = "D:/XXX.png";
        bool isUploading = false;

        UnityEngine.Debug.Log("開始!!");

        ThreadPool.QueueUserWorkItem((o) =>
        {
            using (WebClient client = new WebClient())
            {
                client.Credentials = new NetworkCredential("kim", "123");
                client.UploadFile("ftp://127.0.0.1/XXX.png", "STOR", filename);
            }
            isUploading = true;
        });

        while (!isUploading)
        {
            UnityEngine.Debug.Log("上傳中!!");
            yield return new WaitForSeconds(0.1f);
        }

        UnityEngine.Debug.Log("結束!!");
    }
}

使用ftpWebRequest的第二種:
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Threading;
using UnityEngine;

public class XXX : MonoBehaviour
{
    void Start()
    {
        StartCoroutine(FTPUpload2());
    }
    
    void Update()
    {

    }
    
    IEnumerator FTPUpload2()
    {
        var filename = "D:/XXX.png";
        bool isUploading = false;

        UnityEngine.Debug.Log("開始!!");

        ThreadPool.QueueUserWorkItem((o) =>
        {
            FtpWebRequest ftpWebRequest = (FtpWebRequest)WebRequest.Create("ftp://127.0.0.1/XXX.png");

            ftpWebRequest.Method = WebRequestMethods.Ftp.UploadFile;
            ftpWebRequest.Credentials = new NetworkCredential("kim", "123");
            ftpWebRequest.UsePassive = true;
            ftpWebRequest.UseBinary = true;
            ftpWebRequest.KeepAlive = true;

            StreamReader sourceStream = new StreamReader(filename);
            byte[] fileBytes = File.ReadAllBytes(filename);
            sourceStream.Close();
            ftpWebRequest.ContentLength = fileBytes.Length;

            Stream requestStream = ftpWebRequest.GetRequestStream();
            requestStream.Write(fileBytes, 0, fileBytes.Length);
            requestStream.Close();

            FtpWebResponse ftpWebResponse = (FtpWebResponse)ftpWebRequest.GetResponse();
            ftpWebResponse.Close();

            isUploading = true;
        });

        while (!isUploading)
        {
            UnityEngine.Debug.Log("上傳中!!");
            yield return new WaitForSeconds(0.1f);
        }

        UnityEngine.Debug.Log("結束!!");
    }
}

2018年2月22日 星期四

Unity內使用Tab切換輸入框的方式

話說我之前有個案子要求可以使用Tab來切換到其他輸入框,如果是Windows程式或是網頁,根本不用為此花功夫;但是Unity就沒這種天經地義的功能了,只好來動手寫一個試看看。

先確立觀念是當按下Tab時,便讓Unity去Focus到某一個介面上,如果先前已點在某一個介面上,便要像是順理成章般地到下一個介面去。

因此做了個簡單的介面:

然後在程式一開始時便Focus在帳號輸入框上,接著按Tab便可在兩個輸入框內不斷切換:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.EventSystems;

public class XXX : MonoBehaviour {

    private GameObject account;
    private GameObject password;

    void Start ()
    {
        account = GameObject.Find("Account");
        password = GameObject.Find("Password");

        EventSystem.current.SetSelectedGameObject(account);
    }

    void Update ()
    {
        if (Input.GetKeyDown(KeyCode.Tab))
        {
            if (EventSystem.current.currentSelectedGameObject == account)
            {
                EventSystem.current.SetSelectedGameObject(password);
            }
            else if (EventSystem.current.currentSelectedGameObject == password)
            {
                EventSystem.current.SetSelectedGameObject(account);
            }
        }
    }
}


所以其實很單純,使用兩個API來取得當前Focus的GUI、和設定想Focus的GUI就好了。

EventSystem.current.currentSelectedGameObject可取得當前Focus的GUI。
EventSystem.current.SetSelectedGameObject()可設定想Focus的GUI。

用這方式可以再配合一個List來做出在複雜輸入介面上輕鬆切換的效果,我想可能有更好的觀念和寫法吧,但我自己是喜歡這個很直觀又方便修改的架構。

2017年11月10日 星期五

Unity內使用UDP方式來進行連線和傳送資料

網路連線、TCP和UDP那些大道理就不講了,不懂的人建議在看這篇前先去觀看一下相關的定義。

這邊簡單來講,有兩個程式分別在兩台電腦上,希望可以透過網路來互相傳遞資料,因此其中之一的作法便是透過UDP方式來進行傳送資料。

可以分開寫成兩個程式,也可以全部都寫在同一個程式內,然後自己再另外寫可選擇當Server或Client的程式。我是常常寫在一個程式內,然後再外加可選擇的方式來定義Server和Client。

所以以下單純來看Script,有Server和Client的Script。

先來看當Client的Script:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.Net;
using System.Net.Sockets;

public class ClientController : MonoBehaviour {

    private IPEndPoint ipEndPoint;
    private UdpClient udpClient;
    private byte[] sendByte;

    void Start ()
    {
        ipEndPoint = new IPEndPoint(IPAddress.Parse("127.0.0.1"), 5555);
        udpClient = new UdpClient();
        SendUDPData("這是要傳送的資料");
    }

    void Update ()
    {

    }

    void SendUDPData(string tempData)
    {
        sendByte = System.Text.Encoding.UTF8.GetBytes(tempData);
        udpClient.Send(sendByte, sendByte.Length, ipEndPoint);
    }
}
這邊範例的IPAddress是使用Local端,當然也可以使用網際網路的網址。
仔細看程式碼的話,便會知道Client單方面的向Server發送了資料。

再來看當Server的Script:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.Net;
using System.Net.Sockets;
using System.Threading;

public class ServerController : MonoBehaviour {

    private IPEndPoint ipEndPoint;
    private UdpClient udpClient;
    private Thread receiveThread;
    private byte[] receiveByte;
    private string receiveData = "";

    void Start ()
    {
        ipEndPoint = new IPEndPoint(IPAddress.Any, 5555);
        udpClient = new UdpClient(ipEndPoint.Port);

        receiveThread = new Thread(ReceiveData);
        receiveThread.IsBackground = true;
        receiveThread.Start();
    }

    void Update ()
    {

    }

    void ReceiveData()
    {
        while (true)
        {
            receiveByte = udpClient.Receive(ref ipEndPoint);
            receiveData = System.Text.Encoding.UTF8.GetString(receiveByte);

            Debug.Log("接收到:" + receiveData);
        }
    }

    private void OnDisable()
    {
        udpClient.Close();
        receiveThread.Join();
        receiveThread.Abort();
    }

    private void OnApplicationQuit()
    {
        receiveThread.Abort();
    }
}
可以看到Server的接收是寫在Thread裡面,如果不另外開執行緒來進行資料的監聽接收,那程式會卡死在那不會動,這點很重要。

關閉程式時,一定要徹底關閉這個Thread;不然即使程式關閉了,這個Thread還會依然持續執行,相關資訊可自行上網查詢。

這裡寫的是Client發送資料給Server的單方向範例,當然也可以Server發送資料給Client,寫法完全一樣。

因為我想要最直接呈現UDP的寫法,所以相關安全措施大都沒有加入,像Try catch等這些安全機制其實是應該要加入的。

2017年10月31日 星期二

Unity內用Pointer方式來偵測非互動型的UGUI

在UGUI內像是Button或Slidert等這種可以讓使用者操控的介面,都會有Interactable這個設定,同時也可以使用下列的程式碼偵測到點擊:

if (Input.GetMouseButtonDown(0))
{
    if (EventSystem.current.currentSelectedGameObject != null)
    {
        Debug.Log(EventSystem.current.currentSelectedGameObject);
    }
}
但是像Image或Rawimage等這種非互動類型的介面,無法用上述的方式偵測到,但偏偏有需要的話該怎麼辦?

首先想到的是從Raycast Target這個設定著手,但意外地我無法使用2DRaycast等方式去偵測(應該是要可以才對,是我人品太糟?):

然後發現了Pointer和其相關方式,因此使用了一下,效果就和Button的OnClick一樣:



使用EventTrigger有很多種觸發方式,相當好用:

最後,如果即時創造UGUI,並且需要即時賦予點擊時回傳的參數時,上面那些事先設定的方式就沒法用了,因此查詢一下各種作法後,發現下面這種方式比較適合我。

首先在即時創造的UGUI上,有沒有EventTrigger這個Comoponent都可以,有的話也不要事先添加任何UnityEvent:

然後撰寫下面程式,並將Script附加在該UGUI物件下:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class ImageController : MonoBehaviour, IPointerDownHandler, IPointerUpHandler {

    void Start ()
    {

    }

    void Update ()
    {

    }

    public void OnPointerDown(PointerEventData eventData)
    {
        Debug.Log("Point down = " + this.name);
    }

    public void OnPointerUp(PointerEventData eventData)
    {
        Debug.Log("Point up = " + this.name);
    }
}
這樣就等於當點擊這個UGUI時,程式會回傳該UGUI的名稱,或是其他想回傳的相對應資料了。