最美情侣中文字幕电影,在线麻豆精品传媒,在线网站高清黄,久久黄色视频

歡迎光臨散文網(wǎng) 會員登陸 & 注冊

我們來用Unity做一個局域網(wǎng)游戲(上)

2018-09-11 16:43 作者:皮皮關(guān)做游戲  | 我要投稿

作者:ProcessCA


本篇難度:★★★★☆

(請注意,本文非常長,而且為了照顧到對網(wǎng)絡(luò)不太熟悉的童鞋,用了較大篇幅作相關(guān)鋪墊,食用前請做好心理準(zhǔn)備)

前言

大家好。

由純單機到聯(lián)網(wǎng)游戲,是游戲開發(fā)的一個質(zhì)的突破。無論是從涉及技術(shù)的廣度,還是從當(dāng)今的市場需求出發(fā),都是極其有意義的。

完全不涉及網(wǎng)絡(luò)的游戲越來越少了,或多或少都會帶一些聯(lián)網(wǎng)要素

這次就來向大家介紹如何利用Unity制作一款局域網(wǎng)(LAN)游戲——五子棋。

麻雀雖小,五臟俱全。

由于項目代碼過于臃腫龐大,不會放出所有源碼,只會專注于講述實現(xiàn)思路。對細節(jié)感興趣的同學(xué)可以在文章結(jié)尾下載本項目服務(wù)器與客戶端的源碼。技術(shù)有限,僅供參考。

先上一波效果圖:

以下是游戲的服務(wù)器(使用傳說中的大黑框,方便調(diào)試)

以下是游戲的客戶端(UI像不像電視遙控器?)

當(dāng)然服務(wù)器與客戶端互相獨立,客戶端使用Unity開發(fā),服務(wù)器使用控制臺開發(fā)。

在同一臺電腦上只能運行一個服務(wù)器,但是可以運行多個客戶端。

當(dāng)然服務(wù)器與客戶端互相獨立,客戶端使用Unity開發(fā),服務(wù)器使用控制臺開發(fā)。

在同一臺電腦上只能運行一個服務(wù)器,但是可以運行多個客戶端。



項目分析

聯(lián)機游戲肯定缺不了服務(wù)器跟客戶端,采用的同步方式為狀態(tài)同步,以下功能可供參考:

客戶端的功能:接受用戶輸入,把用戶輸入的結(jié)果封裝成消息發(fā)送給服務(wù)器。

服務(wù)器的功能:接受客戶端的消息,處理游戲邏輯后把結(jié)果以消息形式反饋給特定的客戶端。


所謂的消息(Message)就是根據(jù)雙方制定的協(xié)議(Protocol)來封裝與解析的數(shù)據(jù)。把消息封裝與解析的過程叫做序列化(Serialize)與反序列化(Deserialize)。

中國人和中國人說話,要遵循漢語的的語法結(jié)構(gòu),使用漢語的發(fā)音。當(dāng)我們和外國人交流時,就要適用外國的語言了,遵循外國的語法結(jié)構(gòu)和發(fā)音。其實這就是一種協(xié)議,只不過我們稱之為語言。


開發(fā)準(zhǔn)備

網(wǎng)絡(luò)協(xié)議

網(wǎng)絡(luò)通訊部分用到了TCP/IP網(wǎng)絡(luò)協(xié)議,客戶端與服務(wù)器所有操作都基于這個協(xié)議在不同的計算機之間進行數(shù)據(jù)傳輸。

Transmission Control Protocol/Internet Protocol的簡寫,中譯名為傳輸控制協(xié)議/因特網(wǎng)互聯(lián)協(xié)議,又名網(wǎng)絡(luò)通訊協(xié)議,是Internet最基本的協(xié)議、Internet國際互聯(lián)網(wǎng)絡(luò)的基礎(chǔ),由網(wǎng)絡(luò)層的IP協(xié)議和傳輸層的TCP協(xié)議組成。TCP/IP 定義了電子設(shè)備如何連入因特網(wǎng),以及數(shù)據(jù)如何在它們之間傳輸?shù)臉?biāo)準(zhǔn)。

序列化工具

要實現(xiàn)在不同計算機進行數(shù)據(jù)(二進制數(shù)據(jù))傳輸,首先需要想清楚的是:如何把客戶端的信息(例如:角色坐標(biāo),角色狀態(tài))序列化為能夠在不同計算機之間傳輸?shù)?strong>二進制數(shù)據(jù)。接著把二進制數(shù)據(jù)傳輸給服務(wù)器,服務(wù)器再把這堆數(shù)據(jù)反序列化為對象進行邏輯處理。

通用的做法是雙方先制定特定的協(xié)議并提供序列化工具。客戶端與服務(wù)器均按照這個協(xié)議中制定的類型來創(chuàng)建特定的對象。

然后用序列化工具把對象序列化為傳輸數(shù)據(jù),或者把傳輸數(shù)據(jù)反序列化為對象。

下圖是一張TCP/IP通信數(shù)據(jù)流程圖:TCP/IP協(xié)議中,數(shù)據(jù)在不同計算機之間的傳輸流程。

現(xiàn)在常用的網(wǎng)絡(luò)通信協(xié)議有Protocol Buffer,Json,Xml等。在這里為了方便易懂,直接利用C#制定通信協(xié)議并且利用C#實現(xiàn)序列化工具類,以下是序列化工具代碼:

using System.Net;

using System.Net.Sockets;

using System.IO;

using System.Runtime.Serialization.Formatters.Binary;

using System.Text;

/// <summary>

/// 網(wǎng)絡(luò)工具類 <see langword="static"/>

/// </summary>

public static class NetworkUtils

{

??? //序列化:obj -> byte[]

??? public static byte[] Serialize(object obj)

??? {

??????? //對象必須被標(biāo)記為Serializable

??????? if (obj == null || !obj.GetType().IsSerializable)

??????????? return null;

??????? BinaryFormatter formatter = new BinaryFormatter();

??????? using (MemoryStream stream = new MemoryStream())

??????? {

??????????? formatter.Serialize(stream, obj);

??????????? byte[] data = stream.ToArray();

??????????? return data;

??????? }

??? }

?

??? //反序列化:byte[] -> obj

??? public static T Deserialize<T>(byte[] data) where T : class

??? {

??????? //T必須是可序列化的類型

??????? if (data == null || !typeof(T).IsSerializable)

??????????? return null;

??????? BinaryFormatter formatter = new BinaryFormatter();

??????? using (MemoryStream stream = new MemoryStream(data))

??????? {

??????????? object obj = formatter.Deserialize(stream);

??????????? return obj as T;

??????? }

??? }

}

?


消息協(xié)議

實現(xiàn)了序列化工具,接下來就是根據(jù)具體游戲設(shè)計不同的協(xié)議:所謂協(xié)議,就是客戶端跟服務(wù)器都要遵守的規(guī)范。比如:為了記錄角色位置,在Unity客戶端可以直接使用Vector3類保存角色的位置信息,但是服務(wù)器一般不會寫在Unity客戶端上,甚至不會用C#編寫。這時服務(wù)器跟客戶端必須制定一個雙方都可以使用的類型進行數(shù)據(jù)交互。

對于五子棋游戲來說:服務(wù)器采用房間機制比較合理,同一個服務(wù)器上運行多個房間,每個房間擁有自己的狀態(tài),棋盤與玩家,互不干涉。根據(jù)這種情況設(shè)計了一套簡單的消息類型枚舉。

首先是棋子類型,棋子類型既可以用于表示棋子本身,也可以表示勝利的一方:

/// <summary>

??? /// 棋子類型

??? /// </summary>

??? public enum Chess

??? {

??????? //棋子類型

??????? None, //空棋

??????? Black,//黑棋

??????? White,//白棋

?

??????? //以下用于勝利判斷結(jié)果和操作結(jié)果

??????? Draw, //平局

??????? Null, //表示無結(jié)果(用于用戶操作失敗情況下的返回值)

??? }

?

下圖為服務(wù)器與客戶端的消息類型協(xié)議:


/// <summary>

??? /// 消息類型

??? /// </summary>

??? public enum MessageType

??? {

??????? None,???????? //空類型

??????? HeartBeat,??? //心跳包驗證

???????

??????? //以下為玩家操作請求類型

??? ????Enroll,?????? //注冊

??????? CreatRoom,??? //創(chuàng)建房間

??????? EnterRoom,??? //進入房間

??????? ExitRoom,???? //退出房間

??????? StartGame,??? //開始游戲

??????? PlayChess,??? //下棋

??? }

?

每種玩家操作請求類型都對應(yīng)一個類(Class),這個類包含客戶端向服務(wù)器發(fā)送的屬性,也包含服務(wù)器向客戶端回應(yīng)的屬性。

以創(chuàng)建房間為例:


[Serializable]???????? //加上C#自帶的可序列化特性就可以把該類型序列化了

??? public class CreatRoom

??? {

??????? public int RoomId; //房間號碼,客戶端向服務(wù)器發(fā)送的屬性

??????? public bool Suc;?? //是否成功,服務(wù)器向客戶端發(fā)送的屬性

??? }

?

由于協(xié)議類型過多,這里只展示一種協(xié)議的序列化與反序列化。


byte[] data = NetworkUtils.Serialize(new CreatRoom(){ RoomId = 8848 }); //序列化

CreatRoom room =? NetworkUtils.Deserialize<CreatRoom>(data);??????????? //反序列化

?

明白了協(xié)議與序列化,接下來就是構(gòu)建我們局域網(wǎng)游戲的架構(gòu)。

由于游戲是采用狀態(tài)同步:客戶端不會把玩家的操作直接發(fā)給服務(wù)器,例如:用戶在客戶端的棋盤上下了一步棋,客戶端會先把用戶的輸入轉(zhuǎn)化為棋盤上坐標(biāo),然后再把用戶當(dāng)前的棋子類型跟這一步棋的坐標(biāo)發(fā)給服務(wù)器,由服務(wù)器去做游戲邏輯再把結(jié)果返回給客戶端,最后客戶端執(zhí)行生成棋子的操作。

接下來就是介紹實現(xiàn)這些部分的思路與大概的實現(xiàn)流程:協(xié)議,客戶端與服務(wù)器。


開發(fā)過程

協(xié)議

首先我們用VisualStudio創(chuàng)建一個消息協(xié)議的C#類庫項目。

然后在項目屬性里面把輸出類型改為類庫(DLL)。

對于unity版本低于2017的,應(yīng)該把目標(biāo)框架改為.Net Framework 3.5及以下

然后建立一個命名空間:Multiplay,并把上面的協(xié)議類型枚舉放在命名空間中即可。

以下客戶端向服務(wù)器之間協(xié)議的具體類型:

[Serializable]

??? public class Enroll

??? {

??????? public string Name;//姓名

?

??????? public bool Suc;?? //是否成功

??? }

?

??? [Serializable]

??? public class CreatRoom

??? {

??????? public int RoomId; //房間號碼

?

??????? public bool Suc;?? //是否成功

??? }

?

??? [Serializable]

??? public class EnterRoom

??? {

??????? public int RoomId;????? //房間號碼

?

??????? public Result result;?? //結(jié)果

??????? public enum Result

??????? {

??????????? None,

??????????? Player,

??????????? Observer,

??????? }

??? }

?

??? [Serializable]

??? public class ExitRoom

??? {

??????? public int RoomId;? //房間號碼

?

??????? public bool Suc;??? //是否成功

??? }

?

??? [Serializable]

??? public class StartGame

??? {

??????? public int RoomId;? ??????????//房間號碼

?

??????? public bool Suc;????????????? //是否成功

??????? public bool First;??????????? //是否先手

??????? public bool Watch;??????????? //是否是觀察者

??? }

?

??? [Serializable]

??? public class PlayChess

??? {

??????? public int RoomId;?????? //房間號碼

? ??????public Chess Chess;????? //棋子類型

??????? public int X;??????????? //棋子坐標(biāo)

??????? public int Y;??????????? //棋子坐標(biāo)

?

??????? public bool Suc;???????? //操作結(jié)果

??????? public Chess Challenger; //勝利者

??? }

?

每一個類型都會有客戶端給服務(wù)器發(fā)送的屬性,也會有服務(wù)器給客戶端響應(yīng)的屬性。

由于五子棋游戲復(fù)雜度的原因,這些協(xié)議類型已經(jīng)夠用。當(dāng)我們編寫完協(xié)議后就可以生成解決方案,在項目的bin目錄下把生成的DLL放進Unity客戶端并且把控制臺服務(wù)器添加對該DLL的引用。

要記住,每次更新協(xié)議之后一定要保證客戶端與服務(wù)器使用的協(xié)議相同,否則會造成兩邊序列化與反序列化出現(xiàn)問題。



獲取IP地址

在此介紹網(wǎng)絡(luò)通信之前,我們編寫一個方法并放在之前的NetworkUtils類中,可以快速獲得本機IPv4地址(用于表示在網(wǎng)絡(luò)上的位置),不然就得在cmd中手動輸入ipconfig指令查看本機IPv4地址。在之后需要IPv4地址的時候,直接調(diào)用該方法即可,代碼如下:

/// <summary>

??? /// 獲取本機IPv4,獲取失敗則返回null

??? /// </summary>

??? public static string GetLocalIPv4()

??? {

??????? string hostName = Dns.GetHostName(); //得到主機名

??????? IPHostEntry iPEntry = Dns.GetHostEntry(hostName);

??????? for (int i = 0; i < iPEntry.AddressList.Length; i++)

??????? {

??????????? //從IP地址列表中篩選出IPv4類型的IP地址

??????????? if (iPEntry.AddressList[i].AddressFamily == AddressFamily.InterNetwork)

??????????????? return iPEntry.AddressList[i].ToString();

??????? }

??????? return null;

??? }

?

OK,有了這些工具并知道了基礎(chǔ)的概念,那么如何實現(xiàn)網(wǎng)絡(luò)通信?

我們是基于TCP/IP協(xié)議在不同計算機之間進行網(wǎng)絡(luò)通訊,但是我們總得有一個可以用C#調(diào)用的接口:就是服務(wù)器跟客戶端進行網(wǎng)絡(luò)操作的API方法。在System.Net.Sockets下,C#為我們貼心地封裝了一個網(wǎng)絡(luò)通訊的API類型:Socket(網(wǎng)絡(luò)套接字)類型。



服務(wù)器網(wǎng)絡(luò)

我們首先創(chuàng)建一個靜態(tài)類:Server,其中包含服務(wù)器的所有網(wǎng)絡(luò)操作。

在Server提供一個Start方法用于實例化Socket對象:

//實例化Socket類型 參數(shù)1:使用ipv4進行尋址 參數(shù)2:使用流進行數(shù)據(jù)傳輸 參數(shù)3:基于TCP協(xié)議

Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);

?

創(chuàng)建好socket對象之后需要把socket對象與IP終端對象(包含IP地址與端口號)進行綁定。以此確定服務(wù)器在網(wǎng)絡(luò)空間中的位置與服務(wù)器這個程序所用的端口號。

如果把IP地址比作一間房子 ,端口就是出入這間房子的門。
使用端口號,可以找到一臺設(shè)備上唯一的一個程序。 所以如果需要和某臺計算機建立連接的話,只需要知道IP地址或域名即可,但是如果想和該臺計算機上的某個程序交換數(shù)據(jù)的話,還必須知道該程序使用的端口號。


//IP終端

IPEndPoint point = new IPEndPoint(IPAddress.Parse(ip), 8848);

socket.Bind(point); //套接字綁定IP終端

socket.Listen(0);?? //開始監(jiān)聽來自其他計算機的連接

?

通過以上操作,服務(wù)器的Socket就已經(jīng)具備監(jiān)聽其他計算機網(wǎng)絡(luò)連接的功能了,接下來就準(zhǔn)備實現(xiàn)服務(wù)器最核心的功能:等待客戶端連接,接收客戶端數(shù)據(jù)。

服務(wù)器概念

服務(wù)器采用開房間(Room)的機制(比如LOL中的一局比賽)進行游戲,并且會有一個集合保存所有玩家(Player)的,有一個字典保存所有房間,有一個回調(diào)方法隊列。

先上一波Server類的數(shù)據(jù)結(jié)構(gòu):

using Multiplay; //使用協(xié)議

/// <summary>

/// <see langword="static"/>

/// </summary>

public static class Server

{

??? public static Dictionary<int, Room> Rooms;????????????????? //游戲房間集合

?

??? public static List<Player> Players;???????????????????????? //玩家集合

?

??? private static Socket _serverSocket;??????????????????????? //服務(wù)器socket

}



稍后會介紹并用到這些屬性與類型。

雖然服務(wù)器直接利用TCP協(xié)議與客戶端進行長連接,但是服務(wù)器除了會保存玩家狀態(tài),數(shù)據(jù)收發(fā)邏輯與HTTP相似。在HTTP中,服務(wù)器不會保存客戶端的狀態(tài),也不會主動向客戶端發(fā)送信息,只有在客戶端向服務(wù)器請求數(shù)據(jù)的時候,服務(wù)器才會向客戶端發(fā)送響應(yīng)數(shù)據(jù)。

關(guān)于HTTP與TCP/IP可參考:(HTTP與TCP的區(qū)別和聯(lián)系 - CSDN博客)https://blog.csdn.net/u013485792/article/details/52100533

我們首先制作兩個關(guān)鍵的數(shù)據(jù)類型用于保存關(guān)鍵數(shù)據(jù)。

服務(wù)器數(shù)據(jù)類型

Player類型,包含客戶端Socket與玩家的基本信息:

public class Player

{

??? public Socket Socket; //網(wǎng)絡(luò)套接字

?

??? public string Name;?? //玩家名字

?

??? public bool InRoom;?? //是否在房間中

?

??? public int RoomId;??? //所處房間號碼

?

??? public Player(Socket socket)

??? {

??????? Socket = socket;

??????? Name = "Player Unknown";

??????? InRoom = false;

??????? RoomId = 0;

??? }

?

??? public void EnterRoom(int roomId)

??? {

??????? InRoom = true;

??????? RoomId = roomId;

??? }

?

??? public void ExitRoom()

??? {

??????? InRoom = false;

??? }

}

?

Room類型,包含房間號碼,房間狀態(tài),容納人數(shù),玩家與觀察者的集合:


public class Room

{

??? public enum RoomState

??? {

??????? Await,? //等待

??????? Gaming? //對局開始

??? }

?

??? //房間ID

??? public int RoomId = 0;

??? //房間棋盤信息

??? public GamePlay GamePlay;

??? //房間狀態(tài)

??? public RoomState State = RoomState.Await;

?

??? //最大玩家數(shù)量

??? public const int MAX_PLAYER_AMOUNT = 2;

??? //最大觀察者數(shù)量

??? public const int MAX_OBSERVER_AMOUNT = 2;

?

??? public List<Player> Players = new List<Player>(); //玩家集合

??? public List<Player> OBs = new List<Player>();???? //觀察者集合

?

??? public Room(int roomId)?????????????????????????? //構(gòu)造

??? {

??????? RoomId = roomId;

??????? GamePlay = new GamePlay();

??? }

}

?

等待客戶端連接

服務(wù)器的思路就是:開啟服務(wù)器監(jiān)聽后,先開啟一個線程(Thread)不斷接受(Accept)客戶端Socket的連接。每當(dāng)有一個客戶端連接上服務(wù)器,服務(wù)器會獲取客戶端的Socket并且啟動一個線程去接收(Receive)這個客戶端發(fā)送的信息。

服務(wù)器上一旦發(fā)生異常且沒有處理,等于服務(wù)器直接掛掉。所有連接上服務(wù)器的客戶端全部會失去連接。所以對于服務(wù)器來說要非常謹慎地編寫關(guān)鍵部分的代碼。

我們繼續(xù)在Server類中編寫,以下是等待客戶端代碼:

? ? ? ? //在初始化方法中開啟等待玩家線程

??????? Thread thread = new Thread(_Await) { IsBackground = true };

??????? thread.Start();

?

??? //等待客戶端方法

??? private static void _Await()

??? {

??????? Socket client = null;

?

??????? while (true)

??????? {

??????????? try

??????????? {

??????????????? //同步等待,程序會阻塞在這里

??????????????? client = _serverSocket.Accept();

?

??????????????? //獲取客戶端唯一鍵

??????????????? string endPoint = client.RemoteEndPoint.ToString();

?

??????????????? //新增玩家

??????????????? Player player = new Player(client);

??????????????? Players.Add(player);

?

??????????????? Console.WriteLine($"{player.Socket.RemoteEndPoint}連接成功");

?

??????????????? //創(chuàng)建特定類型的方法

??????????????? ParameterizedThreadStart receiveMethod =

?????????????????? new ParameterizedThreadStart(_Receive);? //Receive方法在后面實現(xiàn)

?

??????????????? Thread listener = new Thread(receiveMethod) { IsBackground = true };

???????????????

??????????????? listener.Start(player); //開啟一個線程監(jiān)聽該客戶端發(fā)送的消息

??????????? }

??????????? catch (Exception ex)

??????????? {

??????????????? Console.WriteLine(ex.Message);

??????????? }

??????? }

??? }

?

通過這個方法,我們已經(jīng)實現(xiàn)等待客戶端連接。雖然我們還沒有開始編寫客戶端代碼。

不急,一步步來,學(xué)習(xí)開發(fā)網(wǎng)絡(luò)游戲的過程已經(jīng)足夠有趣(十分辛苦)了。
(后面的坑還大的很呢,能從頭到尾看完的都是大佬)

封裝數(shù)據(jù)包

在編寫收發(fā)消息的方法之前,還有一個封裝數(shù)據(jù)包的過程。我們是利用Socket基于TCP網(wǎng)絡(luò)協(xié)議,通過流(Stream)傳輸數(shù)據(jù),網(wǎng)絡(luò)流中只能傳輸二進制,并且網(wǎng)絡(luò)流中的數(shù)據(jù)像水流一樣傳輸,我們怎么知道接收數(shù)據(jù)一次該接收多少字節(jié)的Byte?

以下是一個簡易的封裝數(shù)據(jù)的方法以便于解決以上問題:

? ? /// <summary>

??? /// 封裝數(shù)據(jù)

? ??/// </summary>

??? private static byte[] _Pack(MessageType type, byte[] data = null)

??? {

??????? List<byte> list = new List<byte>();

??????? if (data != null)

??????? {

??????????? list.AddRange(BitConverter.Getbytes((ushort)(4 + data.Length)));//消息長度2字節(jié)

??????????? list.AddRange(BitConverter.Getbytes((ushort)type));???????????? //消息類型2字節(jié)

??????????? list.AddRange(data);??????????????????????????????????????????? //消息內(nèi)容n字節(jié)

??????? }

??????? else

??????? {

??????????? list.AddRange((ushort)4);??? ?????????????????????//消息長度2字節(jié)

??????????? list.AddRange((ushort)type);????????????????????? //消息類型2字節(jié)

??????? }

??????? return packer.ToArray();

??? }

?

我們的消息有一個消息總長度,消息類型,消息本體。

總長度用于確定我們該一次從網(wǎng)絡(luò)流中接收多少長度的數(shù)據(jù),消息類型代表該把消息本體反序列化成什么類型。消息本體本身就是一個byte數(shù)組,必須通過反序列化變成一個具體的對象。

簡單來說,數(shù)據(jù)包就是對序列化后消息的封裝,通過這層封裝后,服務(wù)器與客戶端可以進行正常的讀寫操作。


接收客戶端數(shù)據(jù)

接收消息這里有個回調(diào)事件機制:在把數(shù)據(jù)包拆成:消息長度,消息類型之后,我們可以通過不同的消息類型去執(zhí)行不同的回調(diào)方法。

在回調(diào)方法中,把消息本體(此時依然為Byte)當(dāng)做參數(shù)傳入進去,然后在回調(diào)方法內(nèi)部把消息本體反序列化成對象進行使用。

以下為封裝了一個回調(diào)方法的委托(函數(shù)指針),與一個封裝之后的回調(diào)類型:

//回調(diào)委托

public delegate void ServerCallBack(Player client, byte[] data);

?

//回調(diào)類型

public class CallBack

{

??? public Player Player;

?

??? public byte[] Data;

?

??? public ServerCallBack ServerCallBack;

?

??? public CallBack(Player player, byte[] data, ServerCallBack serverCallBack)

??? {

??????? Player = player;

??????? Data = data;

??????? ServerCallBack = serverCallBack;

??? }

?

??? public void Execute()

??? {

??????? ServerCallBack(Player, Data);

??? }

}

?

在服務(wù)器接收客戶端消息之前,我們應(yīng)該把回調(diào)方法存入一個字典中。在Server類型中加入一個回調(diào)方法隊列(線程安全),一個消息類型字典與一個注冊回調(diào)事件的方法。并且在之前的Start方法中初始化這些屬性。

? ? private static ConcurrentQueue<CallBack> _callBackQueue;????????? //回調(diào)方法隊列

?

??? private static Dictionary<MessageType, ServerCallBack> _callBacks

???? ???= new Dictionary<MessageType, ServerCallBack>();????????????? //消息類型與回調(diào)方法

?

??? /// <summary>

??? /// 注冊消息回調(diào)事件

??? /// </summary>

??? public static void Register(MessageType type, ServerCallBack method)

??? {

??????? if (!_callBacks.ContainsKey(type))

? ??????????_callBacks.Add(type, method);

??????? else

??????????? Console.WriteLine("注冊了相同的回調(diào)事件");

??? }

?

并且在服務(wù)器啟動之前就把回調(diào)方法注冊好,以下方法寫在一個新的類型:Network中:


? ? /// <summary>

??? /// 啟動服務(wù)器

??? /// </summary>

??? /// <param name="ip">IPv4地址</param>

??? public Network(string ip)

??? {

??????? //注冊

??????? Server.Register(MessageType.HeartBeat, _HeartBeat);

??????? Server.Register(MessageType.Enroll, _Enroll);

??????? Server.Register(MessageType.CreatRoom, _CreatRoom);

??????? Server.Register(MessageType.EnterRoom, _EnterRoom);

??????? Server.Register(MessageType.ExitRoom, _ExitRoom);

??????? Server.Register(MessageType.StartGame, _StartGame);

??????? Server.Register(MessageType.PlayChess, _PlayChess);

??????? //啟動服務(wù)器

??????? Server.Start(ip);

??? }

?

至于以上的回調(diào)事件,我們先在Network類中創(chuàng)建好即可,之后再進行填充。

當(dāng)然,把所有的邏輯操作放在回調(diào)事件中執(zhí)行顯然不合適,本項目只是為了方便演示,沒有進行更多邏輯數(shù)據(jù)分離與架構(gòu)設(shè)計。正常的商業(yè)項目中,會抽象更多的層,不同的層進行不同的操作,甚至用的語言都不一樣。很多要保證高效率或者并發(fā)的地方通常會使用C++,而邏輯層可能使用python或者go。

線程安全

因為服務(wù)器對每個客戶端都會開啟線程并接受信息,對于接受信息后觸發(fā)的回調(diào)事件而言,顯然不能在多個單獨的線程中執(zhí)行,得有存放入一個隊列(線程安全),由一個獨立的線程進行執(zhí)行。

特別是極端情況可能會造成同一個房間中的玩家數(shù)據(jù)出現(xiàn)異常,此處不采用鎖(lock),而是采用一個線程安全的隊列:ConcurrentQueue。游戲開始時就開啟一個單獨的線程專門執(zhí)行回調(diào)事件隊列,從而把回調(diào)事件放在單一的線程中執(zhí)行,不同線程只需要往這個隊列里添加回調(diào)事件即可。

以下就是執(zhí)行回調(diào)事件線程的代碼:

? ? ? ? //在開啟Await線程后,開啟回調(diào)方法線程

??????? Thread handle = new Thread(_Callback) { IsBackground = true };

??????? handle.Start();

?

??? private static void _Callback()

??? {

??????? while (true)

??????? {

??????????? if (_callBackQueue.Count > 0)

??????????? {

??????????????? //使用TryDequeue保證線程安全

??????????????? if (_callBackQueue.TryDequeue(out CallBack callBack))

??????????????? {

??????????????????? //執(zhí)行回調(diào)

??????????????????? callBack.Execute();

??????????????? }

??????????? }

??????????? //讓出線程

??????????? Thread.Sleep(10);

??????? }

??? }

?

好的,回到Server類,繼續(xù)實現(xiàn)我們的Receive方法,基于我們上面封裝數(shù)據(jù)包的過程,我們也得以相同的格式把數(shù)據(jù)拆出來。

我們先讀取4個字節(jié)的包頭,根據(jù)解析包頭的數(shù)據(jù)確定該數(shù)據(jù)包的長度,從而繼續(xù)接收包體(消息本體),以下是實現(xiàn)代碼:

? ? private static void _Receive(object obj)

??? {

??????? Player player = obj as Player;

??????? Socket client = player.Socket;

???????

??????? //持續(xù)接受消息

??????? while (true)

??????? {

??????????? //解析數(shù)據(jù)包過程(服務(wù)器與客戶端需要嚴(yán)格按照一定的協(xié)議制定數(shù)據(jù)包)

??????????? byte[] data = new byte[4];

?

?????? ?????int length = 0;??????????????????????????? //消息長度

??????????? MessageType type = MessageType.None;?????? //類型

??????????? int receive = 0;?????????????????????????? //接收信息

?

??????????? try

??????????? {

??????????????? receive = client.Receive(data); //同步接受消息

??????????? }

??????????? catch (Exception ex)

??????????? {

??????????????? Console.WriteLine($"{client.RemoteEndPoint}已掉線:{ex.Message}");

??????????????? player.Offline();

??????????????? return;

??????????? }

?

??????????? //包頭接收不完整

??????????? if (receive < data.Length)

??????????? {

??????????????? Console.WriteLine($"{client.RemoteEndPoint}已掉線");

??????????????? player.Offline();

??????????????? return;

??????????? }

?

??????????? //解析消息過程

??????????? using (MemoryStream stream = new MemoryStream(data))

??????????? {

??????????????? BinaryReader binary = new BinaryReader(stream, Encoding.UTF8);

??????????????? try

??????????????? {

??????????????????? length = binary.ReadUInt16();

??????????????????? type = (MessageType)binary.ReadUInt16();

??? ????????????}

??????????????? catch (Exception)

??????????????? {

??????????????????? Console.WriteLine($"{client.RemoteEndPoint}已掉線");

??????????????????? player.Offline();

??????????????????? return;

??????????????? }

??????????? }

?

??????????? //如果有包體

??????????? if (length - 4 > 0)

??????????? {

??????????????? data = new byte[length - 4];

??????????????? receive = client.Receive(data);

??????????????? if (receive < data.Length)

??????????????? {

??????????????????? Console.WriteLine($"{client.RemoteEndPoint}已掉線");

??????????????????? player.Offline();

??????????????????? return;

??????????????? }

??????????? }

??????????? else

??????????? {

??????????????? data = new byte[0];

??????????????? receive = 0;

??????????? }

?

??????????? Console.WriteLine($"接受到消息, 房間數(shù):{Rooms.Count}, 玩家數(shù):{Players.Count}");

?

??????????? //回調(diào)機制機制

??????????? if (_callBacks.ContainsKey(type))

??????????? {

??????????????? CallBack callBack = new CallBack(player, data, _callBacks[type]);

??????????????? //放入回調(diào)隊列

? ??????????????_callBackQueue.Enqueue(callBack);

??????????? }

??????? }

??? }

?


OK,到這里服務(wù)器的基本功能已經(jīng)實現(xiàn),具體的回調(diào)事件與消息發(fā)送等客戶端網(wǎng)絡(luò)部分搭建完成后再一起介紹。


客戶端網(wǎng)絡(luò)

連接服務(wù)器

創(chuàng)建好unity工程后先新建一個NetworkClient腳本直奔主題先。

首先要想該利用那些C#API連接上服務(wù)器呢?又如何向服務(wù)器收發(fā)數(shù)據(jù)?恩,在這里使用C#的API:TcpClient(連接服務(wù)器建立數(shù)據(jù)流),NetworkStream(在數(shù)據(jù)流中讀寫數(shù)據(jù))

在NetworkClient腳本中,我們?nèi)∠^承Monobehaviour而是把這個類型設(shè)置為靜態(tài)類型。

與服務(wù)器類似,客戶端也有消息回調(diào)字典,與一個待發(fā)送消息隊列,需要發(fā)送消息只需把消息添加進這個隊列,然后提供有專門的協(xié)程(Coroutine)去發(fā)送。

以下是部分NetworkClient類的部分屬性:


? ? using Multiplay; //使用協(xié)議

?

??? /// <summary>

??? /// 客戶端網(wǎng)絡(luò)狀態(tài)枚舉

??? /// </summary>

??? private enum ClientState

??? {

??????? None,??????? //未連接

??????? Connected,?? //連接成功

??? }

?

??? //消息類型與回調(diào)字典

??? private static Dictionary<MessageType, CallBack> _callBacks =

??????? new Dictionary<MessageType, CallBack>();

??? //待發(fā)送消息隊列

??? private static Queue<byte[]> _messages;

??? //當(dāng)前狀態(tài)

??? private static ClientState _curState;

??? //向服務(wù)器建立TCP連接并獲取網(wǎng)絡(luò)通訊流

??? private static TcpClient _client;

??? //在網(wǎng)絡(luò)通訊流中讀寫數(shù)據(jù)

??? private static NetworkStream _stream;

?

??? //目標(biāo)ip

??? private static IPAddress _address;

??? //端口號

??? private static int _port;

?

連接服務(wù)器之前肯定要先確定服務(wù)器的ip地址,端口號服務(wù)器跟客戶端使用一個自定義的就行(0-65536),最好是8848這種比較靠后的數(shù)字,像1024之前的多半被操作系統(tǒng)分配給了其他應(yīng)用程序。以下是ip地址與端口號初始化方法:

? ? /// <summary>

??? /// 初始化網(wǎng)絡(luò)客戶端

??? /// </summary>

??? public static void Init(string address = null, int port = 8848)

??? {

??????? //連接上后不能重復(fù)連接

??????? if (_curState == ClientState.Connected)

??????????? return;

??????? //如果為空則默認連接本機ip的服務(wù)器

??????? if (address == null)

??????????? address = NetworkUtils.GetLocalIPv4();

?

??????? //類型獲取失敗則取消連接

??????? if (!IPAddress.TryParse(address, out _address))

??????????? return;

??? }

?

因為客戶端同時擔(dān)任收發(fā)數(shù)據(jù)的責(zé)任,我們得在Unity的不同協(xié)程(利用C#迭代器實現(xiàn)的類似Update的機制)中同時收發(fā)數(shù)據(jù)。

Unity協(xié)程中不建議用同步方法,大部分同步方法會主線程阻塞或?qū)е滤姥h(huán)

因為NetworkClient類型是靜態(tài)類型,無法直接使用StartCoroutine,因此我們可以提供一個繼承自Monobehaviour的類型去提供這個方法。

以下是這個類型的實現(xiàn),使用單例模式,提供一個程序退出時調(diào)用的委托來關(guān)閉網(wǎng)絡(luò)連接:

? ? public class NetworkCoroutine : MonoBehaviour

??? {

??????? public Action ApplicationQuitEvent;

?

??????? private static NetworkCoroutine _instance;

?

??????? // 場景單例(不隨場景改變而銷毀)

??????? public static NetworkCoroutine Instance

??????? {

??????????? get

??????????? {

??????????????? if (!_instance)

??????????????? {

??????????????????? GameObject socketClientObj = new GameObject("NetworkCoroutine");

??????????????????? _instance = socketClientObj.AddComponent<NetworkCoroutine>();

??????????????????? DontDestroyOnLoad(socketClientObj);

??????????????? }

??????????????? return _instance;

??????????? }

??????? }

?

??????? // 程序退出

??????? private void OnApplicationQuit()

??????? {

??????????? if (ApplicationQuitEvent != null)

??????????????? ApplicationQuitEvent();

??????? }

???? }

?

然后我們在NetworkClient創(chuàng)建一個連接服務(wù)器的方法(Connect),下面是關(guān)鍵代碼:


? ? ? ? //以下代碼放在一個叫Connect的協(xié)程方法中

??????? _client = new TcpClient();

?

??????? //異步連接服務(wù)器

?????? ?IAsyncResult async = _client.BeginConnect(_address, _port, null, null);

??????? while (!async.IsCompleted)

??????? {

??????????? Debug.Log("連接服務(wù)器中");

??????????? yield return null;

??????? }

??????? //結(jié)束異步

??????? _client.EndConnect(async);

??????? //獲取網(wǎng)絡(luò)流

??????? _stream = _client.GetStream();

?

??????? _curState = ClientState.Connected;

??????? _messages = new Queue<byte[]>();

??????? Debug.Log("連接服務(wù)器成功");

?

??????? //設(shè)置異步發(fā)送消息

??????? NetworkCoroutine.Instance.StartCoroutine(_Send());

??????? //設(shè)置異步接收消息

? ??????NetworkCoroutine.Instance.StartCoroutine(_Receive());

??????? //設(shè)置退出事件

??????? NetworkCoroutine.Instance.ApplicationQuitEvent +=

?????????? () => { _client.Close(); _curState = ClientState.None; };

?

現(xiàn)在我們啟動服務(wù)器,并在客戶端新建一個Network類型腳本,放在場景中,然后可以在Start方法中調(diào)用NetworkClient的連接服務(wù)器方法嘗試連接服務(wù)器。

好了。到目前為止,我們已經(jīng)實現(xiàn)了消息的序列化、服務(wù)器房間的搭建以及服務(wù)器與客戶端的基本通信。

介于字數(shù)原因,客戶端gameplay部分與收發(fā)數(shù)據(jù)操作放在下一篇介紹。



最后想系統(tǒng)學(xué)習(xí)游戲開發(fā)的童鞋,歡迎訪問?http://www.levelpp.com/

游戲開發(fā)攪基QQ群:869551769??

微信公眾號:皮皮關(guān)

我們來用Unity做一個局域網(wǎng)游戲(上)的評論 (共 條)

分享到微博請遵守國家法律
定边县| 当雄县| 正宁县| 绵竹市| 大宁县| 彭阳县| 安龙县| 崇文区| 平泉县| 高青县| 黄浦区| 临潭县| 赤壁市| 洛南县| 大厂| 麻江县| 施甸县| 青铜峡市| 新乡市| 桃源县| 兴仁县| 基隆市| 达孜县| 河曲县| 永康市| 乃东县| 南安市| 遂昌县| 北流市| 无极县| 孝感市| 义马市| 涞水县| 五莲县| 江山市| 吉隆县| 渝中区| 铜山县| 阜康市| 清徐县| 四子王旗|