tcp unity 图片_用 Unity 做个游戏(七) - TCP Socket 客户端
前言
這真的是最后一篇有關(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)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 一文说清楚PS里的复制PS复制是什么
- 下一篇: 实现option上下移动_用jQuery