日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

tcp unity 图片_用 Unity 做个游戏(七) - TCP Socket 客户端

發(fā)布時(shí)間:2023/12/4 编程问答 30 豆豆
生活随笔 收集整理的這篇文章主要介紹了 tcp unity 图片_用 Unity 做个游戏(七) - TCP Socket 客户端 小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,幫大家做個(gè)參考.

前言

這真的是最后一篇有關(guān)基礎(chǔ)框架的文章了!

寫到這里已經(jīng)第七篇了orz之前的其實(shí)還是挺枯燥的,都是些基礎(chǔ)方面的東西,并看不到什么有趣的內(nèi)容

可能是我把事情想的太復(fù)雜了吧,所有東西都想做到能力范圍內(nèi)的最好,尤其是這些底層框架層次的東西

不過這些東西真的很重要,小游戲的話可能不會(huì)明顯,Unity的一大優(yōu)勢便在于可以快速地產(chǎn)出游戲原型來,我這個(gè)項(xiàng)目整了這么久就一個(gè)TestView,里面居然只有兩個(gè)按鈕!233

我這些東西也是考慮了許多生產(chǎn)環(huán)境中遇到過的問題,不敢說是最優(yōu),我也還是在學(xué)習(xí)嘛XD

嘛,等把網(wǎng)絡(luò)框架也搭起來,我們就能正式開始寫游戲相關(guān)的邏輯啦~

網(wǎng)絡(luò)通信

我們這游戲是個(gè)多人在線實(shí)時(shí)對戰(zhàn)的游戲,之前的坑里就是網(wǎng)絡(luò)這塊給搞崩了,重新來設(shè)計(jì)

網(wǎng)絡(luò)這塊使用原生TCP Socket進(jìn)行通訊,自定協(xié)議。這里主要先介紹客戶端,先把協(xié)議定下來,這樣之后介紹服務(wù)端的時(shí)候就不會(huì)和客戶端有太大耦合了。當(dāng)然一開始的話還是先弄一個(gè)最簡單的服務(wù)端,本項(xiàng)目計(jì)劃使用Node.js開發(fā)

最簡單的服務(wù)端

在某個(gè)端口上創(chuàng)建一個(gè)TCP服務(wù)器,接收客戶端傳來的消息,拼接一個(gè)字符串后返回。

代碼如下:

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);復(fù)制代碼

客戶端設(shè)計(jì)

用兩個(gè)類,一個(gè)相對底層的SFTcpClient,用戶不直接使用這個(gè)類,而是通過SFNetworkManager加一層封裝,這是個(gè)單例類,可以在游戲運(yùn)行過程中隨時(shí)訪問網(wǎng)絡(luò),還有就是為了以后可能不僅僅使用一個(gè)SfTcpClient,封裝之后可以更優(yōu)雅地管理多個(gè)TCP客戶端。

SFNetworkManager

先看SFNetworkManager,管理著若干個(gè)SFTcpClient,通過后者的以下接口:

|方法|說明|

|--|--|

|void init(string, int, SFClientCallback, SFSocketStateCallback)|根據(jù)指定的IP地址,端口以及相關(guān)回調(diào)初始化|

|void uninit()|關(guān)閉TCP客戶端|

|void sendData(string)|往服務(wù)器發(fā)送數(shù)據(jù)|

|bool isReady|服務(wù)器是否就緒|

首先是作為一個(gè)單例類應(yīng)該有的內(nèi)容:私有的構(gòu)造函數(shù),唯一的實(shí)例,獲取實(shí)例的方法:

private SFNetworkManager(){}

private static sm_instance = null;

public static SFNetworkManager getInstance(){

if (null == sm_instance)

{

sm_instance = new SFNetworkManager();

}

return sm_instance;

}復(fù)制代碼

然后是連接初始化

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);

});

}復(fù)制代碼

void onRecvMsg(string)是處理服務(wù)端推送消息的回調(diào)函數(shù)

void onRecvMsg(string msg){

SFUtils.log("收到了" + msg);

}復(fù)制代碼

現(xiàn)在問題來了,因?yàn)橄⒒卣{(diào)函數(shù)是在Socket子線程里調(diào)用的,Unity里不允許在子線程中對場景中的物體進(jìn)行修改,所以要稍加改造,讓這些消息在主線程中處理。

用一個(gè)隊(duì)列,子線程中收到的消息全加入到這個(gè)隊(duì)列,把內(nèi)容存在內(nèi)存里,然后主線程通過update函數(shù)定期檢查隊(duì)列中是否還有未處理的信息,有的話就全部取出來處理。

void onRecvMsg(string msg){

m_recvQueue.Enqueue(msg);

}

void update(){

while (m_recvQueue.Count > 0)

{

string data = m_recvQueue.Dequeue();

SFUtils.log("收到了" + data);

}

}復(fù)制代碼

SFTcpClient

使用C# TCP Socket的異步實(shí)現(xiàn)。數(shù)據(jù)收發(fā)的子線程由系統(tǒng)管理。

所有的方法都有對應(yīng)的一對BeginXX和EndXX,以接收數(shù)據(jù)為例:

try

{

if (!m_socket.Connected)

{

throw new Exception("Socket is not connected");

}

byte[] data = new byte[1024]; // 以1024字節(jié)為單位接收數(shù)據(jù)

m_socket.BeginReceive(data, 0, data.Length, SocketFlags.None, result =>

{

int length = m_socket.EndReceive(result); // length為實(shí)際接收到的數(shù)據(jù)長度

if (length > 0)

{

m_callback(Encoding.UTF8.GetString(data)); // 轉(zhuǎn)換為字符串并調(diào)用回調(diào)

}

else

{

// length為0說明網(wǎng)絡(luò)已斷開

m_socket.close();

SFUtils.logWarning("網(wǎng)絡(luò)連接中斷");

dispatcher.dispatchEvent(SFEvent.EVENT_NETWORK_INTERRUPTED);

}

}, null);

}

catch (Exception e)

{

SFUtils.logWarning("網(wǎng)絡(luò)連接中斷:" + e.Message);

}復(fù)制代碼

其他像是連接,發(fā)送都大同小異,具體的完整代碼可以查看文章末尾的完整代碼鏈接。

自定協(xié)議

數(shù)據(jù)的首發(fā)暫時(shí)就先這樣(當(dāng)然有很多坑,比如因?yàn)槲沂褂迷鶷CP Socket來傳輸數(shù)據(jù)包,數(shù)據(jù)多的時(shí)候必然會(huì)產(chǎn)生粘包的情況,所以必須手動(dòng)分包,這個(gè)之后再說,和接下來的內(nèi)容關(guān)系不大,要加的話直接在SFTcpClient里的sendData()方法和socketRecv()方法里修改就是了)

網(wǎng)絡(luò)中傳輸?shù)臄?shù)據(jù)使用JSON字符串,發(fā)送和接收的時(shí)候客戶端和服務(wù)端分別各自進(jìn)行序列化和反序列化,這里先只討論客戶端的實(shí)現(xiàn)。

Unity提供了一個(gè)JsonUtility類,有了這個(gè)類我們就能方便地進(jìn)行對象和JSON之間的序列化和反序列化了。主要使用的是兩個(gè)方法:

|方法名|作用|

|--|--|

|string JsonUtility.ToJson(object)|把一個(gè)對象轉(zhuǎn)化成JSON字符串|

|T JsonUtility.FromJson(string)|把一個(gè)JSON字符串轉(zhuǎn)化成指定類型的對象,如果出錯(cuò)則拋出異常|

請求

請求類型均繼承自基類SFBaseRequestMessage,舉一個(gè)例子:

// 基類

public class SFBaseRequestMessage

{

public int pid; // 協(xié)議號(hào)

public string uid; // 用戶唯一ID

};

// 用戶登陸登出

[Serializable]

public class SFRequestMsgUnitLogin : SFBaseRequestMessage

{

public SFRequestMsgUnitLogin(){ pid = 1; }

public int loginOrOut;

};復(fù)制代碼

響應(yīng)

響應(yīng)類型是類似的,每個(gè)請求類型一定對應(yīng)一個(gè)響應(yīng)類型,但反過來卻不一定,即一個(gè)協(xié)議擁有請求類型是擁有響應(yīng)類型的必要非充分條件。

同樣是上面那個(gè)登陸的協(xié)議:

// 基類

public class SFBaseResponseMessage : ISFEventData // 為了讓響應(yīng)結(jié)果也可以方便地作為事件數(shù)據(jù)傳遞

{

public int pid; // 協(xié)議號(hào)

public int retCode; // 錯(cuò)誤代碼,0表示成功

};

// 用戶登陸登出

[Serializable]

public class SFResponseMsgUnitLogin : SFBaseResponseMessage

{

public const string pName = "socket_1"; // pName作為SFEvent的事件名稱

public SFReponseMsgUnitLogin(){ pid = 1; }

};復(fù)制代碼

發(fā)送和接收

發(fā)送非常簡單,創(chuàng)建一個(gè)sendMessage()方法,接收參數(shù)類型為請求基類SFBaseRequestMessage,先序列化然后直接丟給TCP Client來處理發(fā)送即可。

public void sendMessage(SFBaseRequestMessage req){

string data = JsonUtility.ToJson(req);

m_client.sendData(data);

}復(fù)制代碼

接收稍微復(fù)雜點(diǎn)兒,分兩步,首先把原始字符串轉(zhuǎn)成SFBaseResponseMessage,獲取其協(xié)議號(hào)pid,然后根據(jù)不同的pid再轉(zhuǎn)成具體的響應(yīng)類型。

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 更多協(xié)議

else

{

SFUtils.logWarning("不能識(shí)別的協(xié)議號(hào): {0}", 0, pid);

obj = null;

}

if (obj != null)

{

dispatcher.dispatchEvent(pName, obj);

}

}復(fù)制代碼

然后在其他地方添加相應(yīng)協(xié)議的監(jiān)聽即可:

SFNetworkManager.getInstance().dispatcher.addEventListener(SFResponseMsgUnitLogin.pName, onRecvMsg);復(fù)制代碼

回調(diào)函數(shù)一定在主線程中被調(diào)用,所以可以在里面放心地修改游戲場景。

測試程序

創(chuàng)建一個(gè)這樣的UI0701

點(diǎn)擊連接服務(wù)器的按鈕,嘗試連接服務(wù)器:

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 = "服務(wù)器連接成功";

}

else

{

m_infoMsg = "服務(wù)器連接失敗";

}

});

m_mgr.dispatcher.addEventListener(SFEvent.EVENT_NETWORK_INTERRUPTED, onInterrupt);

m_mgr.dispatcher.addEventListener(SFResponseMsgUnitLogin.pName, onRecvMsg);復(fù)制代碼

在此之前啟動(dòng)服務(wù)端程序的話就會(huì)成功連接至服務(wù)器。0702

同時(shí)會(huì)收到來自服務(wù)端的消息"Hello!",當(dāng)然這個(gè)不符合我們的協(xié)議,console面板可以看到程序無法解析這個(gè)字符串,并忽略。

然后點(diǎn)擊發(fā)送消息按鈕,客戶端程序會(huì)發(fā)送一個(gè)測試協(xié)議給服務(wù)端,服務(wù)端就會(huì)收到:

$ node ./

started

有新的連接:::ffff:127.0.0.1

request: {"pid":1,"uid":"abc","loginOrOut":1}復(fù)制代碼

此時(shí)服務(wù)端返回字符串'{"pid":1,"retCode":0}',這就是一個(gè)標(biāo)準(zhǔn)的協(xié)議信息了,程序解析后發(fā)現(xiàn)這是一個(gè)登陸成功的響應(yīng),做出處理:0703

然后按Ctrl+C強(qiáng)制關(guān)閉服務(wù)端程序進(jìn)程,網(wǎng)絡(luò)中斷,客戶端也有對應(yīng)的處理0704

完整代碼

上面貼出的代碼片段由于篇幅限制只保留了關(guān)鍵部分,完整的代碼可在我的github上找到

總結(jié)

以上是生活随笔為你收集整理的tcp unity 图片_用 Unity 做个游戏(七) - TCP Socket 客户端的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網(wǎng)站內(nèi)容還不錯(cuò),歡迎將生活随笔推薦給好友。