遊戲伺服器2025年初架設與研究part14收送封包的Json格式與Unity內建JsonUtility相關的問題(支援性)
上次針對如何在Unity的新版TMP Text中使用中文字
而這次則是來分享一下先前有提過的Json文字轉換的問題
首先我們知道Unity內建的Json轉換是JsonUtility
JsonUtility在使用上有一些限制在
算是有優點也有缺點
優點是畢竟是寫在Unity底層的,效能比較好
如果功能沒有問題的情況下,可以比額外安裝的Json轉換插件效能要好上不少
甚至有些插件能差兩倍以上的執行效能
不過自然也是有缺點
就是它有不少不支援的型別
很多時候常常讓伺服器與遊戲端的人打架的其中一個原因,伺服器表示資料明明就有送,遊戲端則是表示我沒收到(伺服器傳的Json格式轉不出來的種種問題或是可能性)
詳細可以參考Unity的官方文件
https://docs.unity3d.com/6000.1/Documentation/Manual/json-serialization.html
那我這邊也把一些重點特別拿出來講
首先我們先前在聊天訊息回傳的時候,就已經有用到基本的功能
可以先看到我們伺服器端的程式碼(Python)
如以上這語法這是我們先前使用的方式
那這邊因為是跟先前相同的
相同的部分我就用截圖來顯示(詳細程式碼在Part11有可以複製)
可以看到我們主要回傳的結構
json.dumps({"type": "chat", "message": msg})
這段就是把後面的內容透過Json轉換成Json字串後傳給使用者(遊戲端)
而我們Unity接收的位置是這樣寫的
ServerPacket packet = JsonUtility.FromJson<ServerPacket>(json);
這行就是我們今天主要想討論的地方
並且假設大家想要使用額外安裝的Json套件,也幾乎就是替換這行就能達到想要的效果
接下來我們看到ServerPacket這個class
這邊可以看到關鍵是我們的class要可序列化
也就是說要加上[Serializable]
那雖然說是只要加上可序列化,就能轉換
但是也是因為這樣,有很多不能被Unity內建支援的型別,則會轉換不出來
而且這個轉換不出來也不會有任何錯誤產生
就是轉換完之後,你的class裡面就缺了某些資料
當然這邊也要提醒一點
就是雖然[Serializable]可序列化在某些情況不加也不會出錯
甚至可以正常的轉換出來
但是正確的做法還是要加的
所以有時候可能會發現忘了加卻還可以正常執行的時候
是確實有可能的,但是還是記得務必要加
那我這邊也簡單的在Unity中示範一些簡單的範例,來說明那些常用的語法是否可以轉換
首先常見的[Serializable]class
包含string int等等常見的型別都是可以的
包含List<int> 或是 int[]這種也都是可以的
但是比較關鍵不行的是Dictionary
通常最常見的就是Dictionary無法正確的轉換
然後有幾項則是不同版本有可能會有不同結果的
詳細可以使用自己的Unity版本進行測試
首先是繼承的多型
這種多數版本應該第一層是沒有問題的
然後private 變數因為也不會被序列化
所以也無法轉成Json字串
如果希望可以轉,又希望保持private
可以使用[SerializeField]加在變數前面
using System;
using System.Collections.Generic;
using UnityEngine;
public class JsonTest : MonoBehaviour
{
void Start()
{
Debug.Log("=====支援的格式=====");
// 測試基本資料結構
TestSimpleClass();
// 測試 List 支援
TestListSupport();
// 測試多型
TestPolymorphism();
TestEnum();
Debug.Log("=====需要[SerializeField]支援的格式=====");
// 測試私有欄位
TestPrivateField();
Debug.Log("=====不支援的格式=====");
// 測試 Dictionary
TestDictionaryFail();
TestMutiArray();
}
[Serializable]
public class SimpleData
{
public string name;
public int score;
public Vector3 position;
}
void TestSimpleClass()
{
SimpleData data = new SimpleData { name = "Player1", score = 100, position = new Vector3(1, 2, 3) };
string json = JsonUtility.ToJson(data);
Debug.Log("SimpleData JSON: " + json);
}
[Serializable]
public class ListData
{
public List<int> scores;
}
void TestListSupport()
{
ListData listData = new ListData { scores = new List<int> { 1, 2, 3, 4 } };
string json = JsonUtility.ToJson(listData);
Debug.Log("ListData JSON: " + json);
}
[Serializable]
public class DictionaryData
{
public Dictionary<string, int> scores = new Dictionary<string, int> {
{ "a", 1 },
{ "b", 2 }
};
}
void TestDictionaryFail()
{
DictionaryData dictData = new DictionaryData();
try
{
string json = JsonUtility.ToJson(dictData);
Debug.Log("DictionaryData JSON: " + json);
}
catch (Exception ex)
{
Debug.LogWarning("DictionaryData 無法序列化: " + ex.Message);
}
}
[Serializable]
public class PrivateFieldTest
{
[SerializeField]
private int hiddenValue = 999;
}
void TestPrivateField()
{
PrivateFieldTest pf = new PrivateFieldTest();
string json = JsonUtility.ToJson(pf);
Debug.Log("PrivateFieldTest JSON (有SerializeField): " + json);
}
[Serializable]
public class Animal
{
public string type;
}
[Serializable]
public class Meow : Animal
{
public string bark = "Meow Meow!";
}
void TestPolymorphism()
{
Animal meow = new Meow { type = "Meow" };
string json = JsonUtility.ToJson(meow);
Debug.Log("Polymorphic JSON: " + json);
//Meow meowJs = JsonUtility.FromJson<Meow>(json);
//Debug.Log($"meowJs type:{meowJs.type} bark: {meowJs.bark}");
}
[Serializable]
public class Enu
{
public Jtag type;
}
[Serializable]
public enum Jtag
{
a,
b,
c,
d,
e,
f,
};
void TestEnum()
{
Enu e = new Enu { type = Jtag.c};
string json = JsonUtility.ToJson(e);
Debug.Log("TestEnum JSON : " + json);
}
[Serializable]
public class MutiArray
{
public int[,] matrix = new int[2, 2] { { 1, 2 }, { 3, 4 } };
}
void TestMutiArray()
{
MutiArray ma =new MutiArray() { matrix = new int[2, 2] { { 1, 2 }, { 3, 4 } } };
string json = JsonUtility.ToJson(ma);
Debug.Log("TestMutiArray JSON : " + json);
}
}
那我這邊也提供一個測試用的腳本
可以在Unity專案內,放置程式碼的地方新增一個新的腳本
名稱我這邊是JsonTest
那內容貼上之後,可以回到Unity場景中
找一個隨便的物件掛上去
之後play就可以看到執行結果的log
這邊有簡單測試幾個常見的結構
並且也是多數伺服器可能會回傳的值
例如這邊可以看到log
這兩種轉換後都是空的內容
表示轉換失敗了
同樣也表示如果伺服器傳這兩種格式的Json資料,你會無法用內建的Json轉換轉出class
這邊當然也可以自己寫一個轉換,或是直接使用其他的Json轉換插件
雖然考慮到效能內建的還是會比較好
所以假設能把伺服器回傳的資料格式也做規範的話
就可以在效能允許的情況下取得想要的資料
雖然這邊只要在Unity內測試,互相轉換成Json之後再轉回class
多數就能很快看出結果
不過這邊也是提供一下如果想讓伺服器端傳回無法轉換的格式結果會是怎樣
這邊為了測試,就簡單的讓伺服器回傳一個dict的資料
首先這張圖是跟上面那張一樣的,為了方便大家知道我是調整哪一段
我會從原本這邊進行一些調整
import asyncio
import websockets
import json
connected = set()
async def echo(websocket):
print("新的連線")
connected.add(websocket)
try:
async for message in websocket:
try:
data = json.loads(message)
if data["type"] == "login":
username = data["username"]
print(f"{username} 登入了")
await websocket.send(json.dumps({"type": "welcome", "message": f"歡迎 {username}"}))
elif data["type"] == "chat":
msg = data["message"]
for conn in connected:
await conn.send(json.dumps({"type": "chat", "message": msg}))
elif data["type"] == "dict_data":
my_dict = {
"score": 999,
"level": 12,
}
test_data = {
"type": "dict_data",
"data": my_dict
}
await websocket.send(json.dumps(test_data))
except Exception as e:
print("解析錯誤", e)
finally:
connected.remove(websocket)
async def main():
async with websockets.serve(echo, "localhost", 8765):
print("WebSocket server started on ws://localhost:8765")
await asyncio.Future() # 永不結束,讓 server 一直跑下去
if __name__ == "__main__":
asyncio.run(main())
主要加上了elif data["type"] == "dict_data":
這一段
所以我們這時候只要在Unity那邊加上一個事件
讓他去call 給我們Websocket送事件,並且type=dict_data
這樣我們就會收到一個伺服器把dict轉成Json格式的字串資料
我們就可以在Unity接收的地方測試轉換了
[Serializable]
public class ServerDictPacket
{
public string type;
public Dictionary<string,int> data;
}
首先準備好我們用來接收的class
這邊雖然我們知道Dictionary無法成功轉出來
不過還是先寫出來
private void OnMessage(byte[] bytes)
{
string json = Encoding.UTF8.GetString(bytes);
try
{
ServerDictPacket packet = JsonUtility.FromJson<ServerDictPacket>(json);
Debug.Log(packet.type);
Debug.Log(packet.data.Count);
}
catch (Exception e)
{
Debug.LogError("Failed to parse server packet: " + e);
}
}
public async Task SendLogin()
{
ClientPacket packet = new ClientPacket
{
type = "dict_data",
};
string json = JsonUtility.ToJson(packet);
await websocket.SendText(json);
}
接下來是調整一下我們先前測試聊天室的語法
當然先前的語法也可以先保留,先註解掉就好
我們先加上這兩個
那這邊注意一下雖然我名字還是用SendLogin()
不過只是沒改名稱而已
其實裡面不是做Login的事情了
我們實際上是送一個type="dict_data"的Websocket事件了
這邊就是對應剛剛我們在伺服器調整的python語法
讓伺服器會回傳dict格式的Json字串了
調整完之後,我們讓websocket連線後就送這個SendLogin()事件
這樣就會收到先前定義的字串
大家也可以先用log印出來確認
內容確實是有完整的dict轉成Json字串的內容的
但是透過ServerDictPacket packet = JsonUtility.FromJson<ServerDictPacket>(json);
這個轉換後
我去印了兩個log
大家應該就可以發現這兩個log
首先第一個log是正確的,我們回傳的資料的type確實是dict_data
但是data的內容,我是去印dict的count
這會導致直接報錯
所以直接被我們的try catch 給攔截了Exception
內容就是System.NullReferenceException: Object reference not set to an instance of an object
這就是很常見的錯誤
主要就是我們對Null做一些操作
很容易就出現這個Error 訊息
所以可以從這邊看到Unity內建確實是不吃Dict的Json字串的
那這邊該怎麼辦呢?
就像先前說的,可以使用額外的插件來解決
有許多Json的插件是可以支援Dict的Json轉換的
只是Unity內建的不支援而已
那如果真的不想要額外裝插件該怎麼辦呢?
這邊也是提供一個,雖然不是那麼推薦,但是勉強能用的方式
就是我們在回傳的時候先轉字串,然後Unity的遊戲端在那個欄位就用String去接收
也就是先把dict透過json.dumps去先轉一次
接收的class格式則是改成string去接收
這樣就不會讓我們的資料在轉換的時候就遺失了
至於之後要怎麼處理這個data的string
就是可以各自發揮的地方了
例如我們這邊調整之後再把它印出來,這時候就不會跳錯誤
而且還能把內容正確的印出來了(當然是string格式)
因為相信不少專案都是多人分工
且不同區塊有不同負責的人
所以確實這個方法也是有用到的地方的
例如網路底層如果不能改的話
那你能改的地方,可能就會是把資料先存成string
至少這樣不會讓資料直接遺失(假設你不能改網路封包相關的語法底層)
當然這個方法也適用於專案內已經有多數地方使用到
且不能隨便重構的狀況(牽一髮動全身)
至於要使用哪種方法,自然是看各自的專案想要怎麼處理
這邊也是分享幾個方法,讓大家在收送伺服器封包的時候有著更多的選擇
並且不會再因為明明伺服器有傳了資料
從Unity裡面卻怎樣都找不到資料的狀況
這次的分享就先到這邊,如有疑問可以再提出來~
留言
張貼留言