tcp unity 图片_用 Unity 做个游戏(七) - TCP Socket 客户端
前言
這真的是最后一篇有關基礎框架的文章了!
寫到這里已經第七篇了orz之前的其實還是挺枯燥的,都是些基礎方面的東西,并看不到什么有趣的內容
可能是我把事情想的太復雜了吧,所有東西都想做到能力范圍內的最好,尤其是這些底層框架層次的東西
不過這些東西真的很重要,小游戲的話可能不會明顯,Unity的一大優勢便在于可以快速地產出游戲原型來,我這個項目整了這么久就一個TestView,里面居然只有兩個按鈕!233
我這些東西也是考慮了許多生產環境中遇到過的問題,不敢說是最優,我也還是在學習嘛XD
嘛,等把網絡框架也搭起來,我們就能正式開始寫游戲相關的邏輯啦~
網絡通信
我們這游戲是個多人在線實時對戰的游戲,之前的坑里就是網絡這塊給搞崩了,重新來設計
網絡這塊使用原生TCP Socket進行通訊,自定協議。這里主要先介紹客戶端,先把協議定下來,這樣之后介紹服務端的時候就不會和客戶端有太大耦合了。當然一開始的話還是先弄一個最簡單的服務端,本項目計劃使用Node.js開發
最簡單的服務端
在某個端口上創建一個TCP服務器,接收客戶端傳來的消息,拼接一個字符串后返回。
代碼如下:
const net = require("net");
net.createServer(function(socket){
console.log("有新的連接:" + socket.remoteAddress);
socket.on("data", function(data){
console.log("request: " + data);
socket.write('{"pid":1,"retCode":0}');
});
socket.on("end", function(data){
console.log("socket end");
});
socket.on("close", function(data){
console.log("連接已斷開");
});
socket.write("Hello!");
}).listen(19621);復制代碼
客戶端設計
用兩個類,一個相對底層的SFTcpClient,用戶不直接使用這個類,而是通過SFNetworkManager加一層封裝,這是個單例類,可以在游戲運行過程中隨時訪問網絡,還有就是為了以后可能不僅僅使用一個SfTcpClient,封裝之后可以更優雅地管理多個TCP客戶端。
SFNetworkManager
先看SFNetworkManager,管理著若干個SFTcpClient,通過后者的以下接口:
|方法|說明|
|--|--|
|void init(string, int, SFClientCallback, SFSocketStateCallback)|根據指定的IP地址,端口以及相關回調初始化|
|void uninit()|關閉TCP客戶端|
|void sendData(string)|往服務器發送數據|
|bool isReady|服務器是否就緒|
首先是作為一個單例類應該有的內容:私有的構造函數,唯一的實例,獲取實例的方法:
private SFNetworkManager(){}
private static sm_instance = null;
public static SFNetworkManager getInstance(){
if (null == sm_instance)
{
sm_instance = new SFNetworkManager();
}
return sm_instance;
}復制代碼
然后是連接初始化
public void init(){
m_client = new SFTcpClient();
m_client.init("127.0.0.1", 19621, onRecvMsg, ret =>
{
dispatcher.dispatchEvent(SFEvent.EVENT_NETWORK_READY, new SFSimpleEventData(ret));
});
// 向上傳遞連接斷開的事件
m_client.dispatcher.addEventListener(SFEvent.EVENT_NETWORK_INTERRUPTED, e =>
{
dispatcher.dispatchEvent(e);
});
}復制代碼
void onRecvMsg(string)是處理服務端推送消息的回調函數
void onRecvMsg(string msg){
SFUtils.log("收到了" + msg);
}復制代碼
現在問題來了,因為消息回調函數是在Socket子線程里調用的,Unity里不允許在子線程中對場景中的物體進行修改,所以要稍加改造,讓這些消息在主線程中處理。
用一個隊列,子線程中收到的消息全加入到這個隊列,把內容存在內存里,然后主線程通過update函數定期檢查隊列中是否還有未處理的信息,有的話就全部取出來處理。
void onRecvMsg(string msg){
m_recvQueue.Enqueue(msg);
}
void update(){
while (m_recvQueue.Count > 0)
{
string data = m_recvQueue.Dequeue();
SFUtils.log("收到了" + data);
}
}復制代碼
SFTcpClient
使用C# TCP Socket的異步實現。數據收發的子線程由系統管理。
所有的方法都有對應的一對BeginXX和EndXX,以接收數據為例:
try
{
if (!m_socket.Connected)
{
throw new Exception("Socket is not connected");
}
byte[] data = new byte[1024]; // 以1024字節為單位接收數據
m_socket.BeginReceive(data, 0, data.Length, SocketFlags.None, result =>
{
int length = m_socket.EndReceive(result); // length為實際接收到的數據長度
if (length > 0)
{
m_callback(Encoding.UTF8.GetString(data)); // 轉換為字符串并調用回調
}
else
{
// length為0說明網絡已斷開
m_socket.close();
SFUtils.logWarning("網絡連接中斷");
dispatcher.dispatchEvent(SFEvent.EVENT_NETWORK_INTERRUPTED);
}
}, null);
}
catch (Exception e)
{
SFUtils.logWarning("網絡連接中斷:" + e.Message);
}復制代碼
其他像是連接,發送都大同小異,具體的完整代碼可以查看文章末尾的完整代碼鏈接。
自定協議
數據的首發暫時就先這樣(當然有很多坑,比如因為我使用原生TCP Socket來傳輸數據包,數據多的時候必然會產生粘包的情況,所以必須手動分包,這個之后再說,和接下來的內容關系不大,要加的話直接在SFTcpClient里的sendData()方法和socketRecv()方法里修改就是了)
網絡中傳輸的數據使用JSON字符串,發送和接收的時候客戶端和服務端分別各自進行序列化和反序列化,這里先只討論客戶端的實現。
Unity提供了一個JsonUtility類,有了這個類我們就能方便地進行對象和JSON之間的序列化和反序列化了。主要使用的是兩個方法:
|方法名|作用|
|--|--|
|string JsonUtility.ToJson(object)|把一個對象轉化成JSON字符串|
|T JsonUtility.FromJson(string)|把一個JSON字符串轉化成指定類型的對象,如果出錯則拋出異常|
請求
請求類型均繼承自基類SFBaseRequestMessage,舉一個例子:
// 基類
public class SFBaseRequestMessage
{
public int pid; // 協議號
public string uid; // 用戶唯一ID
};
// 用戶登陸登出
[Serializable]
public class SFRequestMsgUnitLogin : SFBaseRequestMessage
{
public SFRequestMsgUnitLogin(){ pid = 1; }
public int loginOrOut;
};復制代碼
響應
響應類型是類似的,每個請求類型一定對應一個響應類型,但反過來卻不一定,即一個協議擁有請求類型是擁有響應類型的必要非充分條件。
同樣是上面那個登陸的協議:
// 基類
public class SFBaseResponseMessage : ISFEventData // 為了讓響應結果也可以方便地作為事件數據傳遞
{
public int pid; // 協議號
public int retCode; // 錯誤代碼,0表示成功
};
// 用戶登陸登出
[Serializable]
public class SFResponseMsgUnitLogin : SFBaseResponseMessage
{
public const string pName = "socket_1"; // pName作為SFEvent的事件名稱
public SFReponseMsgUnitLogin(){ pid = 1; }
};復制代碼
發送和接收
發送非常簡單,創建一個sendMessage()方法,接收參數類型為請求基類SFBaseRequestMessage,先序列化然后直接丟給TCP Client來處理發送即可。
public void sendMessage(SFBaseRequestMessage req){
string data = JsonUtility.ToJson(req);
m_client.sendData(data);
}復制代碼
接收稍微復雜點兒,分兩步,首先把原始字符串轉成SFBaseResponseMessage,獲取其協議號pid,然后根據不同的pid再轉成具體的響應類型。
SFBaseResponse obj = null;
obj = JsonUtility.FromJson(data);
if (obj == null)
{
SFUtils.logWarning("不能解析的信息格式:\n" + data);
}
else
{
int pid = obj.pid;
string pName = string.Format("socket_{0}", pid);
if (pid == 1)
{
obj = JsonUtility.FromJson(data)
}
// else if 更多協議
else
{
SFUtils.logWarning("不能識別的協議號: {0}", 0, pid);
obj = null;
}
if (obj != null)
{
dispatcher.dispatchEvent(pName, obj);
}
}復制代碼
然后在其他地方添加相應協議的監聽即可:
SFNetworkManager.getInstance().dispatcher.addEventListener(SFResponseMsgUnitLogin.pName, onRecvMsg);復制代碼
回調函數一定在主線程中被調用,所以可以在里面放心地修改游戲場景。
測試程序
創建一個這樣的UI0701
點擊連接服務器的按鈕,嘗試連接服務器:
m_mgr = SFNetworkManager.getInstance();
m_mgr.init();
m_mgr.dispatcher.addEventListener(SFEvent.EVENT_NETWORK_READY, result =>
{
SFSimpleEventData retCode = result.data as SFSimpleEventData;
if (retCode.intVal == 0)
{
m_infoMsg = "服務器連接成功";
}
else
{
m_infoMsg = "服務器連接失敗";
}
});
m_mgr.dispatcher.addEventListener(SFEvent.EVENT_NETWORK_INTERRUPTED, onInterrupt);
m_mgr.dispatcher.addEventListener(SFResponseMsgUnitLogin.pName, onRecvMsg);復制代碼
在此之前啟動服務端程序的話就會成功連接至服務器。0702
同時會收到來自服務端的消息"Hello!",當然這個不符合我們的協議,console面板可以看到程序無法解析這個字符串,并忽略。
然后點擊發送消息按鈕,客戶端程序會發送一個測試協議給服務端,服務端就會收到:
$ node ./
started
有新的連接:::ffff:127.0.0.1
request: {"pid":1,"uid":"abc","loginOrOut":1}復制代碼
此時服務端返回字符串'{"pid":1,"retCode":0}',這就是一個標準的協議信息了,程序解析后發現這是一個登陸成功的響應,做出處理:0703
然后按Ctrl+C強制關閉服務端程序進程,網絡中斷,客戶端也有對應的處理0704
完整代碼
上面貼出的代碼片段由于篇幅限制只保留了關鍵部分,完整的代碼可在我的github上找到
總結
以上是生活随笔為你收集整理的tcp unity 图片_用 Unity 做个游戏(七) - TCP Socket 客户端的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 一文说清楚PS里的复制PS复制是什么
- 下一篇: 实现option上下移动_用jQuery