茫茫網海中的冷日
         
茫茫網海中的冷日
發生過的事,不可能遺忘,只是想不起來而已!
 恭喜您是本站第 1671273 位訪客!  登入  | 註冊
主選單

Google 自訂搜尋

Goole 廣告

隨機相片
IMG_60D_00173.jpg

授權條款

使用者登入
使用者名稱:

密碼:


忘了密碼?

現在就註冊!

Game Play Maker : [分享]Unity Master Server

發表者 討論內容
冷日
(冷日)
Webmaster
  • 註冊日: 2008/2/19
  • 來自:
  • 發表數: 15771
[分享]Unity Master Server

主服務器 Master Server

Date:2011-11-06 09:06

The Master Server is a meeting place for games that are actively seeking clients, and player clients who want to connect to them. Its purpose is also to hide IP address and port details and perform technical tasks around setting up network connections which might otherwise not be possible, like firewall handling and NAT punch-through.

Master Server 就像一個遊戲大廳,會主動掃瞄想要連接進來的客戶端。其目的是為了使隱藏的 IP 地址和端口也能夠建立網絡連接,像防火牆和網絡穿透。


Each individual running game server provide a Game Type to the Master Server. All games with a matching Game Type are gathered together so that compatible clients can easily view them. When a player connects and queries the Master Server for their matching Game Type, useful information on the server is displayed for the player to see. This helps the player decide which server to connect to. This includes the Game Name, player count and whether or not a password is needed. The two functions used to transmit this data are MasterServer.RegisterHost() for the Server, and MasterServer.RequestHostList() for the player client.


每個運行的遊戲服務器都需要向 Master Server 提供一個遊戲類型.具有相同類型的遊戲被集中在一起以至於客戶端可以很容易查看到,當一個玩家連接 Master Server 尋找遊戲時。Master Server 可以提供該遊戲的詳細訊息,幫助玩家決定連接哪個服務器。 這些訊息包括遊戲名,玩家數量,是否需要密碼。服器通過調用 MasterServer.RegisterHost()傳遞相應訊息,客戶端通過調用 MasterServer.RequestHostList()去獲取相應訊息。

When calling RegisterHost(), you need to pass three arguments - gameTypeName (which is the previously mentioned Game Type), gameName and comment - for the host being registered. RequestHostList() takes as an argument the gameTypeName
of the hosts you are interested in connecting to. All the registered hosts of that type will then be sent to the requesting client. This is an asynchronous operation and the actual host list can be retrieved with PollHostList() which returns the list when it has arrived.

當調用 RegisterHost() 進行註冊時,需要傳遞三個參數- 遊戲類型名(就是上文提到的遊戲類型),遊戲名和註釋。 RequestHostList() 函數將需要連接的遊戲類型名作為參數,所有已註冊主機的訊息將被發送到客戶端。 這是一個非同步操作,可以通過調用 PollHostList() 獲取所有的主機列表。

The NAT punchthrough portion of the Master Server is actually done by a separate entity called the Facilitator. The central Unity Master Server is also running a Facilitator in addition to being a game lobby so it is OK to think of these functionalities as simply Master Server functionalities in that context.

網絡穿透(NAT)部分的功能,是被另外一個從 Master Server 分割出來的叫 Facilitator 的實體所完成的。 Unity 中心服務器上也運行了一個 Facilitator,可以把 Facilitator 看做是 Master Server 的子功能。


The Game Type you set for your game should be unique for your game. This is to prevent clients running a completely different game from seeing your servers and trying to connect to them. So don't use "Game1" or anything generic like that as the gameTypeName. Be specific and unique. If you have different versions of your game out, you could use the comment field to relay that information. That way the client will know if their version is incompatible with some servers in the host list. The comment field can also be used to relay any information you desire. It is a binary data field and can store objects, you just need to cast them properly yourself.

為遊戲所設置的遊戲類型必須是獨一無二的。防止客戶端看到和實際連接上的遊戲不一樣的情況發生。 因此,不要使用"Game1"或者其他像 gameTypeName 這樣普遍的名字,名字一定是要特定的獨特的,如果遊戲有不同的版本,可以用註釋來表明這個信息,這樣的話客戶端可以知道他們的版本是否和主機相匹配。 這是一個二進制的可存儲對象,你需要自己去正確轉換它們。


More advanced functionality can be done by using the comment field and customizing the Master Server so that it will use that information itself. For example you could reserve the first 10 bytes of the comment field for a password and then extract the password in the Master Server when it receives the host update. It can compare and reject the host update if appropriate (password check fails). Of course this would require editing of the Master Server source code itself, but this can be downloaded and modified. For more information about this, skip down the page to the Advanced Section.

可以使用註釋域定制並實現更多高級功能,例如,你可以利用註釋域的前10字節來存儲密碼,Master Server 可以從主機獲取更新並提取密碼,可以進行比較並拒絕主機的更新(密碼檢查錯誤)。 當然,你需要下載並修改 Master Server 的源代碼,有關這方面更多的信息,請跳過該頁並前往 高級部分

Registering a game 註冊遊戲


Before registering a game it is important to set the NAT functionality properly by setting useNat parameter for Network.InitializeServer to true or false depending on the networking capabilities of the host. The user hosting the game should know if he has a NAT address or not and if the game port is connectable on the public internet, so before hosting the game he will indicate if NAT assistance is needed or not.

A server might be started with code similar to this:

根據主機的網絡情況,在註冊遊戲前,最重要的是通過設置 Network.InitializeServer 的參數 useNat 為 true 或者 false 來正確設置 NAT(網絡地址轉換)功能。 在連接遊戲之前,用戶主機應該知道他是否是一個 NAT 網絡地址或者可連接的遊戲端口是否在公網上,他需要知道是否需要 NAT 的協助。

服務器端的啟動代碼看起來像這樣:


function OnGUI() {
if (GUILayout.Button ("Start Server"))
{
// Use NAT punchthrough if no public IP present
Network.InitializeServer(32, 25002, !Network.HavePublicAddress());
MasterServer.RegisterHost("MyUniqueGameType", "JohnDoes game", "l33t game for all");
}
}

Here we just decide if NAT punchthrough is needed based on if the machine has a public address or not. There is a more involved function available called
Network.TestConnection() which can tell you if the host machine can do NAT or not. It also does connectivity testing for public IP addresses to see if a firewall is blocking the game port. Machines which have public IP addresses always pass the NAT test but if the test fails then the host will NOT be connectable to NAT clients. In such a case the user needs to be informed that in order to host a game he needs to set up port forwarding if possible. Usually people with cheap broadband connections will have a NAT address and not be able to set up port forwarding (as they don't have a personal public IP address at all). In these cases, if the NAT test fails, the user should be informed that running a server is inadvisable as no one but clients on the same local network will be able to connect.


這裡我們根據機器是否具有一個公共地址來決定是否需要使用 NAT 穿透,有個函數 Network.TestConnection(),可以告訴我們主機是否可以 NAT。 它可以連接公網 IP,測試是否有防火牆阻止了遊戲端口。 有公網 IP 的主機通常可以通過 NAT 測試,但是如果測試失敗主機將不能連接 NAT 客戶端。 在這種情況下,用戶需要知道,他可能需要建立端口轉發。 通常人們會有一個 NAT 地址,不能設置端口轉發(因為沒有一個專有的公網 IP)。 這種情況下,如果 NAT 測試失敗,用戶會被告知,運行服務器是不行的,但在同一個局域網的客戶端是可以連接的。

If a host enables NAT functionality without needing it, no harm is done and it will still be connectable, however, client which cannot do NAT punchthrough will think they cannot connect to him as he has NAT enabled.

如果一個主機開啟了 NAT 功能但卻不能用,這樣也沒什麼危害,它仍然可以連接。 但是,不能實現 NAT 穿透的客戶端將不能連接到主機,如果它開啟 NAT 的話。

Connecting to a game 連接到遊戲

The host information, HostData, object sent during host registrations or queries contains the following information:

主機信息,主機數據,對像在登記或查詢時包括如下信息:




booleanuseNat Indicates if the host uses NAT punchthrough. // 表示主機是否使用NAT穿透
String gameType The game type of the host. // 主機類型
String gameName The game name of the host. // 主機名稱
int connectedPlayers The amount of currently connected players/clients. // 當前連接的用戶/客戶端數量
int playerLimit The maximum amount of allowed concurrent players/clients. // 最大連接數量
String[]IP The internal IP address of the host. On a server with a public address the external and internal addresses are the same. This is an array as when connecting internally, all the IP addresses associated with all the active interfaces of the machine need to be checked.
主機的內部IP地址。在具有公網IP地址的服務器的外部和內部地址是相同的。內部連接時他是一個數組,與IP地址相關的所有機器的活動接口都需要被查閱。
int port The port of the host. // 主機的端口
boolean passwordProtected Indicates if you need to supply a password to be able to connect to this host.
提示是否需要提供密碼才能鏈接到這台主機。
String comment Any comment which was set during host registration.
主機註冊時的註釋
String guid The network GUID of the host. This is needed when connecting using NAT punchthrough.
主機的GUID。當進行NAT穿透時需要使用上。

This information can be used by clients to see the connection capabilities of the hosts. When NAT is enabled you need to use the GUID of the host when connecting. This is automatically handled for you when you connect to the HostData struct directly. The connect routine might look something like this:

這些信息可以被客戶端用來查看網絡連接能力,當使用 NAT 連接時,需要使用到主機的 GUID。 當你直接連接到主機數據時,這些信息都會被自動使用,這裡有個大致的連接的範例:


function Awake() {
MasterServer.RequestHostList("MadBubbleSmashGame");
}
function OnGUI() {
var data : HostData[] = MasterServer.PollHostList();
// Go through all the hosts in the host list
for (var element in data)
{
GUILayout.BeginHorizontal();
var name = element.gameName + " " + element.connectedPlayers + " / " + element.playerLimit;
GUILayout.Label(name);
GUILayout.Space(5);
var hostInfo;
hostInfo = "[";
for (var host in element.ip)
hostInfo = hostInfo + host + ":" + element.port + " ";
hostInfo = hostInfo + "]";
GUILayout.Label(hostInfo);
GUILayout.Space(5);
GUILayout.Label(element.comment);
GUILayout.Space(5);
GUILayout.FlexibleSpace();
if (GUILayout.Button("Connect"))
{
// Connect to HostData struct, internally the correct method is used (GUID when using NAT).
Network.Connect(element);
}
GUILayout.EndHorizontal();
}
}

This example code prints out all the relevant information of the hosts returned by the Master Server. Other useful data like ping information or geographic location of hosts can be added to this.

這個例子列印出所有主服務器返回主機的相關訊息。 其他的有用的數據,像 ping 訊息或者主機的地理位置可以被加載到這裡。

NAT punchthrough (NAT 穿透)

Bear in mind that NAT punchthrough will not work 100% of the time so certain machines are not suited to run as servers as some clients can connect to them and certain client might have trouble connecting to any NAT server.

要知道 NAT 穿透並不是 100% 都能工作,所以某些機器並不適合做服務器或客戶端。


By default NAT punch through is done with the help of the Master Server but this functionality does not need to rely on the Master Server. The Facilitator is the one who is actually used for the NAT punchthrough routine. If two machines are connected to the Facilitator, one can directly connect to the other if as long as it uses the external IP and port. The Master Server is used to provide this external IP and port information which is otherwise hard to determine. That is why the Master Server and Facilitator are so tightly integrated. The Master Server and Facilitator have the same IP address by default, to change either one use the MasterServer.ipAddress, MasterServer.port,
Network.natFacilitatorIP and Network.natFacilitatorPort

默認情況下 NAT 穿透是通過主服務器的幫助來完成的,但這個功能並不需要依靠 Master Server。 Facilitator 是一個真正用於NAT穿透的程式。只要使用外部 IP 和端口就可以直接連接。 主服務器是用來提供這種外部難以定位的 IP 和端口訊息的。 這就是為什麼主服務器和調解器要如此緊密的結合起來。 主服和調解器有相同的默認 IP,改變它用 MasterServer.ipAddress, MasterServer.port, Network.natFacilitatorIP
Network.natFacilitatorPort

Advanced 高級

The Master Server is a completely separate entity from the Unity editor itself. It can be deployed on Windows, Linux and Mac OS, and anyone can have their own Master Server. Unity Technologies has a dedicated Master Server available for anyone to use which is the default server used.

Master Server 是一個完全獨立的實體。它可以部署到 windows、Linux、macOS。 unity 技術提供一個專門的主服給所有人作為默認的服務器使用。


There might be cases where you want to modify how the Master Server handles information and how it communicates. Maybe you need to do some optimizations of how the Master Server handles host data information from the individual game servers. Maybe you need to limit how many hosts the clients will receive when they request the host list, either numerically or through some kind of filter like location. This would mostly be bandwidth optimizations but anything can be done as long as it communicates properly with the Unity player.

有些情況下,你可能想知道如何修改主服的處理信息,如何進行通信。 或許你需要去做一些優化,如何從主服向個別遊戲服務器處理主機數據。 也許你需要限制主機收到的列表,無論是數字或通過一些實物,如過濾器的位置。 主要是頻寬的優化。只要大家相互團結,妥善溝通,所有事情都可以做到。

To perform any of these modifications, you must build, deploy, and host your own Master Server. Information about how to do this can be found on the Master Server Build page.

要執行任何這些修改,您必須建立,部署和托管您自己的主服務器。有關如何執行此操作可以在
Master Server Build page找到。

頁面最後更新:2011-05-24


原文出處: Master Server 主服务器 - Unity圣典
冷日
(冷日)
Webmaster
  • 註冊日: 2008/2/19
  • 來自:
  • 發表數: 15771
[分享]Building the Unity Networking Servers on your own

構建自己的 Unity 網絡服務器 Building the Unity Networking Servers on your own

Date:2013-06-25 03:29

The source code for all the individual networking servers can be downloaded from the Unity website. This includes the connection tester, facilitator, master server and proxy server.

可以從 Unity 網站下載創建服務器所需的 source code,包含連接測試器、facilitator、主服務器代和理服務器。

All source packages include the RakNet 3.732 networking library which handles the basic networking functions and provides plugins used by the networking servers.

所有源碼包包含了可以處理基本網絡功能的 RakNet 3.732 庫文件,也提供了可以被網絡服務器所使用的插件。

The packages include three different types of project files, ready for compilation:

這個包,包含了三種不同類型的可編譯的項目文件:

  • An Xcode 3.0 project for Mac OS X
  • A Makefile for Linux and Mac OS X

  • A Visual Studio 2008 solution

The Xcode and Visual Studio projects can just be opened, compiled and built. To build with the Makefile just run "make". It should work with a standard compilation setup on Linux and Mac OS X, if you have gcc then it should work. On Linux you might need to install the ncurses library.

The Xcode 和 Visual Studio 項目可以被打開,編譯並創建帶有 Makefile 的項目,需要運行"make"指令進行創建。 這些都可以在 Linux 和 Mac OS X 上的標準編譯平台上正常工作。 如果採用 gcc 進行編譯,Linux 平台需要安裝 ncurses 庫。

Structure 結構

The Master Server 主服務器

The Master Server uses an internal database structure to keep track of host information.

主服務器使用內部數據結構來跟蹤主機的訊息。

Hosts send messages with the RUM_UPDATE_OR_ADD_ROW message identifier and all their host information embedded. This is processed in the OnReceive()
function in the LightweightDatabaseServer.cpp file. This is where all message initially appear and therefore a good place to start if you want to trace how a message is processed. A table is created within the database structure for each game type which is set when you use MasterServer.RegisterHost function. All game types are grouped together in a table, if the table does not exist it is dynamically created in the CreateDefaultTable() function.

主機發送帶有 RUM_UPDATE_OR_ADD_ROW 標識符的消息並嵌入主機訊息,這些消息在 LightweightDatabaseServer.cpp 文件中的 OnReceive() 函數中被處理。 所有的消息被處理的地方,也是開始跟蹤消息如何被處理的好地方,當使用 MasterServer.RegisterHost函數,將會為每一個遊戲類型創建一個數據庫表格。 所有類型的遊戲被組織在這個表格裡,如果表格不存在,可以在調用 CreateDefaultTable() 函數時動態生成。


The host information data is modified by the master server. The IP and port of the game which is registering, as seen by the master server, is injected into the host data. This way we can for sure detect the correct external IP and port in cases where the host has a private address (NAT address). The IP and port in the host data sent by the game server is the private address and port and this is stored for later use. If the master server detects that a client is requesting the host data for a game server and the server has the same IP address then he uses the private address of the server instead of the external one. This is to handle cases where the client and server are on the same local network, using the same router with NAT addresses. Thus they will have the same external address and cannot connect to each other through it, they need to use the private addresses and those will work in this case.


主服務器可以修改主機的訊息數據,被主服務器所看到的已註冊的遊戲的 IP 和端口被灌入到主機數據裡,這樣我們可以確保,當主機使用私有地址(NAT 地址),也可以檢測其正確的外部 IP 和端口。 被主服務器所發送的 IP 和端口將會被存儲以備後用。如果主服務器檢測到客戶端,正在請求一個遊戲服務器的主機數據,而這個服務器同 Master Server 具有相同的IP地址,這時就會轉而使用服務器的私有地址,而不是外部的那個。 這是處理客戶端和服務器在同一個本地網絡的,使用相同的帶 NAT 地址的路由器情況。他們具有相同的外部地址,因此,無法通過它來連接對方,在這種情況下他們需要使用私有地址。

Clients send messages with the ID_DATABASE_QUERY_REQUEST message identifier and what game type they are looking for. The table or host list is fetched from the database structure and sent to the client. If it isn't found and empty host list is sent.

客戶端發送帶有 ID_DATABASE_QUERY_REQUEST 標識符的消息來請求查找的遊戲類型。 從數據庫結構中取出表格或者主機列表並發送到客戶端,如果沒有找到,將發送空的主機表格。

All messages sent to the master server must contain version information which is checked in the CheckVersion()
function. At the moment each version of Unity will set a new master server version internally and this is checked here. So if the master server communication routine will change at any point it will be able to detect older versions here and possibly refer to another version of the master server (if at all needed) or modify the processing of that message to account for differences.

所有發送到主機的消息包必須包含版本訊息,並在 CheckVersion() 函數中被檢查。 每一個 Unity 版本都會設置一個新的內部主服務器版本並在這裡被檢查。 因此,如果主服務器通信程式在任何時候被改變,它將能夠檢測到舊版本同時可能會參考主服務器的其他版本(如果有必要的話)或者修改消息的處理過程去應對差異。

The Facilitator

The facilitator uses the NAT punchthrough plugin from RakNet directly with no modifications. This is essentially just a peer listening on a port with the NAT punchthrough plugin loaded. When a server and a client with NAT addresses are both connected to this peer, they will be able to perform NAT punchthrough to connect to each other. When the
Network.InitializeServer uses NAT, the connection is set up automatically for you.

Facilitator 直接使用了 RakNet 的 NAT 網絡穿透插件。 本質上就是加載 NAT 網絡穿透插件來偵聽一個端口。當一個帶有 NAT 地址的服務器和客戶端都在連接這個端口時,他們可以執行網絡穿透去連接到對方.當調用 Network.InitializeServer使用 NAT,連接會被自動設置。

頁面最後更新: 2011-02-04


Building the Unity Networking Servers on your own 构建自己的Unity网络服务器 - Unity圣典
冷日
(冷日)
Webmaster
  • 註冊日: 2008/2/19
  • 來自:
  • 發表數: 15771
[轉貼]多人遊戲基礎

Unity學習系類筆記5:多人遊戲基礎

1.總覽。

多人遊戲基本結構:Clent/Server,分為Authoritative Server和Non-Authoritative Server兩種,前者客戶端發送消息,服務器端反饋結果,好處是有效防止客戶端作弊,並統一不同客戶端之間的物理表現和互動狀況,缺陷是存在網絡延時,很有可能每發出一個命令要過一段時間才能接收到反饋。解決方法是client-side prediction客戶端預測,客戶端預測服務器的反饋,但不能做出關鍵性事件的預測,比如某個怪物的死亡。關於client-side prediction技術可以到網上查閱相關資料,這裡不贅述。


Non-Authoritative Sever有客戶端自己處理用戶輸入和對像邏輯,不需要要client-side prediction技術。這種情況服務器端要處理的數據量相對比較小。

網絡通信的方法:Remote Procedure Calls(RPC,遠程進程調用)和State Synchronization(狀態同步)。

NAT punchthrough:網絡連接可能是複雜的,比如中間要通過一個帶公共IP的NAT路由器,怎樣把NAT的內部私有IP和外部計算機連接?可以利用一個叫做Facilitator的服務器和private IP接觸並獲知其所用的IP地址和端口號。另外,內部防火牆玩家可以更改設置,外部防火牆也可以使用這種NAT punchthrough技術穿透。


Minimizing Network Bandwidth:最小化網絡開支,只傳輸絕對重要的數據,比如客戶端完全可以獨立處理角色模型而不需要服務器發送數據來同步。另外記住,你可以使用一個自帶關卡標示的RPC調用來讓所有客戶端同時裝載這個關卡。

2.基礎函數。

建立服務器:Network.InitializeServer().


函數原型:static function InitializeServer (connections : int, listenPort : int, useNat : boolean) : NetworkConnectionError
其中connections是允許連接數,useNat是否允許NAT punchthrough連接。

一個初始化例子:


public class example : MonoBehaviour {
void LaunchServer() {
Network.incomingPassword = "HolyMoly";
bool useNat = !Network.HavePublicAddress();
Network.InitializeServer(32, 25000, useNat);
}
}

其中Network.HavePublicAddress()查看是否擁有IPV4公共IP.這是簡單那的測試方法,另外一個更值得研究的方法是Network.TestConnction().

建立連接到服務器:Network.Connect().
熟悉一下Network類總是有好處。

使用Network View組件進行通信,這一點很重要,它讓你可以使用RPC調用和State Synchronization.每個組件有一個唯一的NetworkViewID,每個數據包都對應發送到一個由NetworkViewID指定的Network View所鏈接的對象上。當使用Network.Instantiate()創建Network對像時,會自動分配Network View,不用開發者操心。當然你也通過RPG調用在每個客戶機上實例化一個Network View對象,然後使用Network.AllocateViewID()函數手動分配NetworkViewID。


當你向一個對像添加NetworkView組件,並指定狀態同步,需要選擇NetworkView所監視的組件,這可以是該對象的transform,animation,script,或是rigidbody.如果不使用狀態同步而只是用PRC調用,則state Synchronization設為off即可,只要有一個Network View(不論是否開啟state Synchronization)你都可以使用RPC.

設置state Synchronization為Reliable Delta Compression,為可靠傳輸,數據按序發送接收。

同步監視對象是script時,需要使用OnSerializeNetworkView顯式化序列數據:


function OnSerializeNetworkView (stream : BitStream, info : NetworkMessageInfo) {
var horizontalInput : float = Input.GetAxis ("Horizontal");
stream.Serialize (horizontalInput);
}

此代碼當你本地更新或收到stream的更改時總是寫horizontalInput的值。有時候你在本地更新和收到更新時,需要做不同的事情,此時敲入如下代碼:


function OnSerializeNetworkView (stream : BitStream, info : NetworkMessageInfo) {
var horizontalInput : float = 0.0;
if (stream.isWriting) {
// Sending
horizontalInput = Input.GetAxis ("Horizontal");
stream.Serialize (horizontalInput);
} else {
// Receiving
stream.Serialize (horizontalInput);
// ... do something meaningful with the received variable
}
}

在C#Script中Serialize()的參數是ref類型,在參數horizontalInput的前面加上引用符號ref即可。

OnSerializeNetworkView的調用頻率在Network Manager的SendRate裡設置,默認是15/s.

3.RPC和狀態同步。

如果要調用RPC,所調用函數必須帶有@RPC屬性,如下:


var playerBullet : GameObject;
function Update () {
if (Input.GetButtonDown ("Fire1")) {
networkView.RPC ("PlayerFire", RPCMode.All);
}
}
@RPC
function PlayerFire () {
Instantiate (playerBullet, playerBullet.transform.position, playerBullet.transform.rotation);
}

RPC調用是可靠的有序的通信。

以下是使用Network.AllocateViewID()手動分配ViewID的實例,其中使用RPC調用實例化cubePrefab的遠程調用,ViewID作為該遠程調用的參數傳遞:


public class example : MonoBehaviour {
public Transform cubePrefab;
void OnGUI() {
if (GUILayout.Button("SpawnBox")) {
NetworkViewID viewID = Network.AllocateViewID();
networkView.RPC("SpawnBox", RPCMode.AllBuffered, viewID, transform.position);
}
}
[RPC]
void SpawnBox(NetworkViewID viewID, Vector3 location) {
Transform clone;
clone = Instantiate(cubePrefab, location, Quaternion.identity) as Transform as Transform;
NetworkView nView;
nView = clone.GetComponent();
nView.viewID = viewID;
}
}

那麼,RPC(Remote Procedure Call)的規則和定義到底是什麼?
RPC是某對象的腳本組件中包含的聲明為[RPC]的(JS為@RPC)函數,這個對象必須包含一個Network View組件,並且監視對象是該腳本組件。這樣,其他腳本就可以從遠程通過這個對象調用該RPC.

RPC函數的參數個數任意,但是為了減少網絡消耗提高網絡性能,個數最好優化到最少。
RPC調用(Network.RPC())的參數還應加上RPC函數名稱(string)和RPCMode(接收對象,包括All,Allbuffered,Others,Server等等),接下來才是RPC函數的參數。


RPC調用可以用來在所有客戶端上執行某個事件,或者在兩個特定的部分之間傳遞事件訊息,你也可以創造性的發揮。比如,一個遊戲服務器只有當4個客戶端連接上才開始遊戲,當第四個客戶端連上以後,服務器可以向所有客戶端同時發送RPC從而開始遊戲。一個客戶端可以向所有客戶端發送RPC告知他拿到了某個物品。服務器可以在客戶端連接時發送RPC讓客戶端執行初始化,比如給它一個player number,出生地點,隊伍顏色,等等。客戶端可以發送RPC定制它的啟動設置,比如顏色偏好,已購物品等等。

NetworkMessageInfo結構體中包含timestamp時間戳和sender發送者兩個數據成員。在RPC函數的參數列表中加入一項NetworkMessageInfo,可自動獲取該訊息,而在RPC調用中不用顯示列出該參數。


@RPC
function PrintText (text : String, info : NetworkMessageInfo)
{
Debug.Log(text + " from " + info.sender);
}

RPC Buffer:使用buffer模式可以讓你之前發送的RPC被新連接的客戶端接受。比如向當前玩家發送裝載某個關卡的RPC,過段時間新玩家連接到遊戲,需要裝載當前其他玩家正在進行的關卡。這時新連接的玩家不會錯過裝載該關卡的RPC,因為它被緩存了起來。另外還可以刷新buffer,比如開始新的關卡後新連接的玩家不如從之前的關卡開始一個一個裝載。

State Synchronization的一些細節:
如前所述,狀態同步的對象只能是以下四種類型之一:transform,animation,rigidbody和monobehaviour(也就是script),其中script的需要用onSerializeNetworkView()函數顯示指定監視的變量數據。BitStream.Serialize()可接受的參數可以是bool,char,short,int,float,Quaternion,Vector3和NetworkPlayer.詳情參考官方script手冊中的
BitStream.Serialize().

關於狀態同步兩種方式的選擇:

Reliable Delta Compressed:在屬性級別自動對比當前狀態(比如position和rotation是transform的兩個不同屬性,如果其中之一發生變動,只發送該變動數據),如果無變動則不發送。Unity會利用ACK和NACK來保證UDP包的發送可靠性,丟失的包會等待重傳(是的,Unity傳輸層使用的是UDP協議)。缺陷是網絡不好會導致明顯延時。


Unreliable:不保證可靠性,因此它不是僅僅發送更改的部分數據保留未更改的部分,而是每次更新都發送所有的數據,不管某部分改動還是未改動。這種模式用於遊戲每幀都在變動的情況,比如賽車遊戲。

綜上,遊戲變化頻繁且迅速,而丟一兩個包無關緊要,這時候選擇Unreliable,如果不是頻繁變化,為了節省帶寬,建議選擇Reliable Delta Compressed.

4.Network.Instantiate.


此方法在每個客戶機上實例化一個prefab,並且是buffered型RPC,這樣當一個新的客戶機連接到服務器後,會自動實例化一個prefab.
函數原型:static function Instantiate (prefab : Object, position : Vector3, rotation : Quaternion, group : int) : Object
其中group(組)可以用來設置消息的過濾,如果不需要這個功能則設置為0.

調用此RPC的例子:


public class example : MonoBehaviour {
public Transform playerPrefab;
void OnConnectedToServer() {
Network.Instantiate(playerPrefab, transform.position, transform.rotation, 0);
}
}

可以用Network.RemoveRPCs()函數來去掉buffer區的Network.Instantiate()函數。你可以去掉特定組的RPC(如果Instantiate指定了組的話),也可以去掉第一個NetworkView ID的RPC,因為當實例化發生的時候RPC消息是與第一個Network View連接的。
函數原型:static function RemoveRPCs (playerID : NetworkPlayer, group : int) : void

從RPC buffer裡去掉Instantiation的例子:


public class example : MonoBehaviour {
void OnNetworkInstantiate(NetworkMessageInfo info) {
Debug.Log(networkView.viewID + " spawned");
if (Network.isServer) {
Network.RemoveRPCs(networkView.viewID);
Network.Destroy(gameObject);
}
}
}

組的作用:讓特定(組)的客戶端只能收到特定的消息。比如,如果兩個玩家分隔在物理上互不影響的兩個區域,但是可以進行聊天。這時候他們之間遊戲狀態數據的交換就很有限,但是要保留聊天功能。物理上的遊戲對象的實例化就應該和提供聊天功能的對象的實例化分開,設定為不同的組。

5.Master Server:


在Master Server中可以顯示所有的服務器,這樣客戶端可以在這裡選擇適合的服務器,另外Master Server隱藏端口和IP,解決連接問題,比如防火牆和NAT的punchthrough.
每個遊戲需要向Master Server提交一個Game Type(注意避免和別的遊戲重複),當客戶端選擇好Game Type,Master Server會提供一個包含遊戲人數、可能設置密碼的運行中遊戲列表。
這兩個方法分別是:MasterServer.RegisterHost()(Host端)和MasterServer.RequestHostList()(客戶端).
函數原型:static function RegisterHost (gameTypeName : String, gameName : String, comment : String = "") : void

RegisterHost的例子:


public class example : MonoBehaviour {
void OnGUI() {
if (GUILayout.Button("Start Server")) {
bool useNat = !Network.HavePublicAddress();
Network.InitializeServer(32, 25002, useNat);
MasterServer.RegisterHost("MyUniqueGameType", "JohnDoes game", "l33t game for all");
}
}
}

函數原型:static function RequestHostList (gameTypeName : String) : void

RequestHostList的例子:


public class example : MonoBehaviour
{
void Awake()
{
MasterServer.ClearHostList();
MasterServer.RequestHostList("LarusTest");
}
void Update()
{
if (MasterServer.PollHostList().Length != 0)
{
HostData[] hostData = MasterServer.PollHostList();
int i = 0;
while (i < hostData.Length)
{
Debug.Log("Game name: " + hostData[i].gameName);
i++;
}
MasterServer.ClearHostList();
}
}
}

上面的代碼使用ClearHostList()清除掉由PollHostList()獲取的HostList,然後Request新的HostList,把新的HostList通過PollHostList()存儲在HostData數組中,其中HostData是保存HostList中數據的數據結構,其成員變量如下圖:

HostData結構的數據成員列表

以下是連接MasterServer的進程實例:


function Awake() {
MasterServer.RequestHostList("MadBubbleSmashGame");
}
function OnGUI() {
var data : HostData[] = MasterServer.PollHostList();
// Go through all the hosts in the host list
for (var element in data)
{
GUILayout.BeginHorizontal();
var name = element.gameName + " " + element.connectedPlayers + " / " + element.playerLimit;
GUILayout.Label(name);
GUILayout.Space(5);
var hostInfo;
hostInfo = "[";
for (var host in element.ip)
hostInfo = hostInfo + host + ":" + element.port + " ";
hostInfo = hostInfo + "]";
GUILayout.Label(hostInfo);
GUILayout.Space(5);
GUILayout.Label(element.comment);
GUILayout.Space(5);
GUILayout.FlexibleSpace();
if (GUILayout.Button("Connect"))
{
// Connect to HostData struct, internally the correct method is used (GUID when using NAT).
Network.Connect(element);
}
GUILayout.EndHorizontal();
}
}

可以繼續向其中添加ping值和地理位置等訊息,這裡不再贅述。

關於NAT punchthrough,如果某個客戶端設備在NAT中且沒有NAT punchthrough技術,可以依賴Facilitator,如果做服務器的客戶端和其他客戶端都能連接到這個Facilitator,那麼服務器和客戶端就能進行通信,前提是Facilitator使用外部IP和端口。Master Server可以提供這個外部IP和端口,所以一般Master Sever同時擔當Faciliator的角色,此時默認兩者擁有共同的IP地址。使用MasterServer.ipAddress,MasterServer.port,Network.natFacilitatorIP,Network.natFacilitatorPort修改IP地址和端口號。

Unity提供默認的Master Server服務,但是你也可以自己組建一個Mater Server.方法詳見
官網

6.Debug.

在Network Manager中可以設置debug級別為看到所有進出的交通訊息。

NetworkManager設置


在Inspector和Hierarchy裡觀察遊戲對象的建立以及ViewID等。
Unity可以雙開,你可以讓它運行不同的project.在windows環境只需要再打開一個Unity並打開一個新的project即可。在Mac OS X環境需要在終端輸入以下命令:

/Applications/Unity/Unity.app/Contents/MacOS/Unity -projectPath "/Users/MyUser/MyProjectFolder/"
/Applications/Unity/Unity.app/Contents/MacOS/Unity -projectPath "/Users/MyUser/MyOtherProjectFolder/"


在對Network進行debug的時候,要選中Run In Background選項,因為當你運行兩個實例並在其中一個上debug,另一個會失去焦點,你要確保在後台它也能運行。在Project Setting>Player裡面設置,也可以直接使用Apllication.runInBackground = true語句打開該功能。

在Player設置面板中選中Run In Background

基礎部分學習到此結束,下篇文章學習多人遊戲中的關卡裝載,帶寬優化以及連接測試等。


原文出處:Unity学习系类笔记5:多人游戏基础 - ㊰ ❀ ☀ ☁ ☂ ☃ - 博客频道 - CSDN.NET
冷日
(冷日)
Webmaster
  • 註冊日: 2008/2/19
  • 來自:
  • 發表數: 15771
[轉貼]多人遊戲進階篇

Unity學習系列筆記6:多人遊戲進階篇

這篇筆記學習三個問題:多人遊戲的level loading,連接測試以及帶寬優化。

一.關卡裝載。

必須瞭解的函數:

1.RequireComponent()


//JavaScript實例
// Mark the PlayerScript as requiring a rigidbody in the game object.
//指定此script綁定的對象必須包含rigidbody組件,沒有則自動添加
@script RequireComponent(Rigidbody)
function FixedUpdate() {
rigidbody.AddForce(Vector3.up);
}

//C#實例
[RequireComponent (typeof (Rigidbody))]
public class PlayerScript : MonoBehaviour {
void FixedUpdate() {
rigidbody.AddForce(Vector3.up);
}
}

2.DontDestroyOnLoad()

當裝載一個新的level,舊場景的對象會被銷毀,調用此函數可以在裝載新level時保留某個對象,如果這個對象是GameObject或者Component那麼它的整個transform層都將被保留。


//C#代碼
public class example : MonoBehaviour {
void Awake() {
DontDestroyOnLoad(transform.gameObject);
}
}

3.Network.peerType

Network類的一個數據成員,返回值是NetworkPeerType的枚舉類型,其值有NetworkPeerType.Disconnected//無客戶端連接且沒有初始化服務器
NetworkPeerType.Connecting//正在連接服務器
NetworkPeerType.Server//正在做為服務器運行
NetworkPeerType.Client//正在作為客戶端運行

4.Network.RemoveRPCsInGroup()


清除某個組的RPC buffer的所有RPC調用。

5.Network.SetSendingEnabled (group : int, enabled : boolean)

開啟或關閉某個組的Network View的消息或RPC發送。比如要開始裝載某個新level,與舊level相關的消息或RPC就可以停止發送。

6.Network.IsMessageQueueRunning()


開啟或關閉消息隊列的運行,如果關閉則RPC調用不再執行,狀態同步機制也不工作。

7.Network.SetLevelPrefix(prefix: int)

為所有Network ViewID設置一個前綴。這將有效防止舊level的數據和新levle的數據互相干擾,這一效果不會造成網絡帶寬負擔,但是由於Network ViewID添加了前綴,導致ID池數量有所減少。

8.Object.FindObjectsOfType(type:Type)


返回一個活動的對象列表(數組)。不返回assets和非活動的對象。這個函數會導致系統變慢,建議不要在每幀都使用,大多數情況下你都可以使用Singleton模式(參見C#設計模式)作為替代。


//C#實例
public class example : MonoBehaviour {
void OnMouseDown() {
HingeJoint[] hinges = FindObjectsOfType(typeof(HingeJoint)) as HingeJoint[];
foreach (HingeJoint hinge in hinges) {
hinge.useSpring = false;
}
}
}

9.SendMessage()

GameObject.SendMessage(methodName : string, value : object = null, options : SendMessageOptions = SendMessageOptions.RequireReceiver)
Component.SendMessage(methodName : string, value : object = null, options : SendMessageOptions = SendMessageOptions.RequireReceiver)

調用methodName所指向的方法,若沒有傳遞任何參數被調用的方法可以選擇忽略。當SendMessageOptions設置為RequireReceiver時,若沒有組件接收該調用消息,則列印一條錯誤訊息。


//C#實例
public class example : MonoBehaviour {
void ApplyDamage(float damage) {
print(damage);
}
void Example() {
gameObject.SendMessage("ApplyDamage", 5.0F);
}
}

這些函數在下面的例子中會被用到。

以下是一個簡單的多人遊戲關卡裝載的例子,在關卡裝載過程中必須確保不處理其他的network消息,在裝載完畢一切就緒之前任何消息不得發送。關卡一旦裝載好以後會自動發送一條消息到所有的腳本,通知它們關卡已經裝載完畢。SetLevelPrefix()函數保證自動過濾與新裝載關卡無關的network數據更新,這些無關數據更新可能是比如說上一個關卡的數據。本例還利用組功能分隔開遊戲數據和關卡裝載通信數據。組0用於遊戲數據交通,而組1用於關卡裝載。當關卡正在裝載的時候,關閉組0開啟組1.


//Javascript實例
var supportedNetworkLevels : String[] = [ "mylevel" ];
var disconnectedLevel : String = "loader";
private var lastLevelPrefix = 0;

function Awake ()
{
// Network level loading is done in a separate channel.
DontDestroyOnLoad(this);
networkView.group = 1;
Application.LoadLevel(disconnectedLevel);
}

function OnGUI ()
{
if (Network.peerType != NetworkPeerType.Disconnected)
{
GUILayout.BeginArea(Rect(0, Screen.height - 30, Screen.width, 30));
GUILayout.BeginHorizontal();
for (var level in supportedNetworkLevels)
{
if (GUILayout.Button(level))
{
Network.RemoveRPCsInGroup(0);
Network.RemoveRPCsInGroup(1);
networkView.RPC( "LoadLevel", RPCMode.AllBuffered, level, lastLevelPrefix + 1);
}
}
GUILayout.FlexibleSpace();
GUILayout.EndHorizontal();
GUILayout.EndArea();
}
}

@RPC
function LoadLevel (level : String, levelPrefix : int)
{
lastLevelPrefix = levelPrefix;
// There is no reason to send any more data over the network on the default channel,
// because we are about to load the level, thus all those objects will get deleted anyway
Network.SetSendingEnabled(0, false);
// We need to stop receiving because first the level must be loaded first.
// Once the level is loaded, rpc's and other state update attached to objects in the level are allowed to fire
Network.isMessageQueueRunning = false;
// All network views loaded from a level will get a prefix into their NetworkViewID.
// This will prevent old updates from clients leaking into a newly created scene.
Network.SetLevelPrefix(levelPrefix);
Application.LoadLevel(level);
yield;
yield;
// Allow receiving data again
Network.isMessageQueueRunning = true;
// Now the level has been loaded and we can start sending out data to clients
Network.SetSendingEnabled(0, true);
for (var go in FindObjectsOfType(GameObject))
go.SendMessage("OnNetworkLoadedLevel", SendMessageOptions.DontRequireReceiver);
}

function OnDisconnectedFromServer ()
{
Application.LoadLevel(disconnectedLevel);
}
@script RequireComponent(NetworkView)

二.連接測試。

Network.TestConnection (forceTest : boolean = false) : ConnectionTesterStatus
此函數用於測試網絡連接狀況。
返回值類型ConnectionTesterStatus是枚舉類型,其值如下表所示。

ConnectionTesterStatus的可能取值


其中PublicIPIsConnectable,PublicIPPortBlocked以及PublicIPNoServerStarted是測試是否具有公共IP的結果,這個測試主要用於作為服務器運行的情形,因為即使沒有Public IP,客戶端通過NAT punchthough或者Facilitator等還是可以連接到服務器,但是要作為服務器運行就需要有Public IP.此項測試需要有一個已運行的服務器實例,然後測試服務器嘗試連接該服務器的指定IP和端口。

最下面4項是測試是否具備NAT punchthrough的能力,這個測試既用於服務器也用於客戶端,不需要提前進行設置。其中Full cone type和Address-restricted cone type支持完整的NAT punchthrough功能,Port retricted不能連接到Symmetric,但可以連接到另外三種,Symmetric則只能與前兩種連接。


此函數提供的網絡連接測試功能具有非同步性,上一次調用的測試結果下一次調用才能獲得結果。而且為了保證已完成的測試不被重複進行,下一次測試(比如網絡狀況發生變化時需要進行重新測試)需要給參數forceTest賦值true.

下面是一個javascript寫的測試實例:


var testStatus = "Testing network connection capabilities.";
var testMessage = "Test in progress";
var shouldEnableNatMessage : String = "";
var doneTesting = false;
var probingPublicIP = false;
var serverPort = 9999;
var connectionTestResult = ConnectionTesterStatus.Undetermined;
// Indicates if the useNat parameter be enabled when starting a server
var useNat = false;

function OnGUI() {
GUILayout.Label("Current Status: " + testStatus);
GUILayout.Label("Test result : " + testMessage);
GUILayout.Label(shouldEnableNatMessage);
if (!doneTesting)
TestConnection();
}

function TestConnection() {
// Start/Poll the connection test, report the results in a label and
// react to the results accordingly
connectionTestResult = Network.TestConnection();
switch (connectionTestResult) {
case ConnectionTesterStatus.Error:
testMessage = "Problem determining NAT capabilities";
doneTesting = true;
break;
case ConnectionTesterStatus.Undetermined:
testMessage = "Undetermined NAT capabilities";
doneTesting = false;
break;
case ConnectionTesterStatus.PublicIPIsConnectable:
testMessage = "Directly connectable public IP address.";
useNat = false;
doneTesting = true;
break;
// This case is a bit special as we now need to check if we can
// circumvent the blocking by using NAT punchthrough
case ConnectionTesterStatus.PublicIPPortBlocked:
testMessage = "Non-connectable public IP address (port " +
serverPort +" blocked), running a server is impossible.";
useNat = false;
// If no NAT punchthrough test has been performed on this public
// IP, force a test
if (!probingPublicIP) {
connectionTestResult = Network.TestConnectionNAT();
probingPublicIP = true;
testStatus = "Testing if blocked public IP can be circumvented";
timer = Time.time + 10;
}
// NAT punchthrough test was performed but we still get blocked
else if (Time.time > timer) {
probingPublicIP = false; // reset
useNat = true;
doneTesting = true;
}
break;
case ConnectionTesterStatus.PublicIPNoServerStarted:
testMessage = "Public IP address but server not initialized, "+
"it must be started to check server accessibility. Restart "+
"connection test when ready.";
break;
case ConnectionTesterStatus.LimitedNATPunchthroughPortRestricted:
testMessage = "Limited NAT punchthrough capabilities. Cannot "+
"connect to all types of NAT servers. Running a server "+
"is ill advised as not everyone can connect.";
useNat = true;
doneTesting = true;
break;
case ConnectionTesterStatus.LimitedNATPunchthroughSymmetric:
testMessage = "Limited NAT punchthrough capabilities. Cannot "+
"connect to all types of NAT servers. Running a server "+
"is ill advised as not everyone can connect.";
useNat = true;
doneTesting = true;
break;
case ConnectionTesterStatus.NATpunchthroughAddressRestrictedCone:
case ConnectionTesterStatus.NATpunchthroughFullCone:
testMessage = "NAT punchthrough capable. Can connect to all "+
"servers and receive connections from all clients. Enabling "+
"NAT punchthrough functionality.";
useNat = true;
doneTesting = true;
break;
default:
testMessage = "Error in test routine, got " + connectionTestResult;
}
if (doneTesting) {
if (useNat)
shouldEnableNatMessage = "When starting a server the NAT "+
"punchthrough feature should be enabled (useNat parameter)";
else
shouldEnableNatMessage = "NAT punchthrough not needed";
testStatus = "Done testing";
}
}

如果Server和Client的NAT類型可以被提前獲知,那麼可以不用TestConnection()函數,自己就能寫出一個簡單的比較函數,只要其中之一不是Symmetric類型,兩台機器就可以進行連接。

javascript的例子如下:


function CanConnectTo(type1: ConnectionTesterStatus, type2: ConnectionTesterStatus) : boolean
{
if (type1 == ConnectionTesterStatus.LimitedNATPunchthroughPortRestricted &&
type2 == ConnectionTesterStatus.LimitedNATPunchthroughSymmetric)
return false;
else if (type1 == ConnectionTesterStatus.LimitedNATPunchthroughSymmetric &&
type2 == ConnectionTesterStatus.LimitedNATPunchthroughPortRestricted)
return false;
else if (type1 == ConnectionTesterStatus.LimitedNATPunchthroughSymmetric &&
type2 == ConnectionTesterStatus.LimitedNATPunchthroughSymmetric)
return false;
return true;
}

其中用到的一些函數和變量:

1.Network.TestConnectionNAT(forceTest : boolean = false)

專用於測試NAT punchthrough,即使機器沒有NAT IP(內部IP)地址,而擁有一個外部公共地址。

2.Time.time


Time.time的值是遊戲開始運行到當前的時間。下面是一個利用Time.time實現子彈按一定時間間隔連續發射的例子:


public class example : MonoBehaviour {
public GameObject projectile;
public float fireRate = 0.5F;
private float nextFire = 0.0F;
void Update() {
if (Input.GetButton("Fire1") && Time.time > nextFire) {
nextFire = Time.time + fireRate;
GameObject clone = Instantiate(projectile, transform.position, transform.rotation) as GameObject;
}
}
}

三.優化帶寬。

帶寬的使用量取決於你是否使用Reliable Delta Compressed或者Unreliable類型的狀態同步機制。Unreliable模式下,整個被同步的對象在每次同步更新以後都按序傳送,同步更新頻率取決於Network.sendRate的值,默認情形下這個值為15.Unreliable模式下數據更新非常頻繁但是不保證數據包的送達,如果有丟包處理方法是簡單的忽略。此模式用於遊戲舞台變化非常頻繁迅速,並且少量的丟包可忽略的情形。Unreliable的帶寬使用量還是比較大的,減少帶寬的方法可以是降低數據傳送率,15的默認值對一般遊戲來說是個合適的取值。

Reliable Delta Compressed模式下數據按序傳輸,且保證數據包不丟失,如果有丟包現象,會等待數據包重傳,如果數據包沒有按序到達,會存入緩衝等待尚未到達的數據。數據等待重傳會一定程度的降低數據傳輸效率,但是因為數據是Delta Compressed,只有更改的數據才會被更新同步,如果數據無變化則沒有數據包傳送。所以Reliable Delta Compressed模式的效果取決於遊戲的場景變化頻繁程度。


另一個問題是,什麼樣的數據需要被同步?這是一個足夠遊戲設計者發揮創意的問題,處理的好壞可以決定帶寬的優化情況。一個例子是動畫數據的同步,如果動畫組件被Network View組件監視,動畫屬性的變化會被嚴格同步,動畫的每一幀在每個客戶端都表現相同。雖然這在某些情況下是可取的,但是一般情況下角色只需要知道角色處於走路,跳躍,跑步等等什麼樣的狀態就足夠,僅僅發送需要播放哪些動畫幀的訊息即可完成同步。這比同步整個動畫組件要節省帶寬得多。

再者,什麼時候需要同步?當兩個玩家在分隔在地圖兩個不同的區域,他們在一段時間內不能見到彼此,那麼這時候的同步完全沒有多大必要。而在地圖一個區域內物理上有機會互相接觸的玩家則需要同步。這樣可以節省部分帶寬。這個概念被稱為相關集(Relevant Sets),即在某個特定時間,某個特定的客戶端只與整個遊戲的一個子集(subset)相關。根據客戶端的相關集來進行數據同步可以使用RPC調用,後者可以對同步進行更多的控制。

在關卡裝載的過程中不需要擔心數據同步佔用帶寬的優化問題,因為每個玩家只需等待其他玩家都裝載完畢即可,所以在關卡裝載時常常涉及到傳輸大量的數據,比如圖像和音頻數據等。


原文出處:Unity学习系列笔记6:多人游戏进阶篇 - ㊰ ❀ ☀ ☁ ☂ ☃ - 博客频道 - CSDN.NET
冷日
(冷日)
Webmaster
  • 註冊日: 2008/2/19
  • 來自:
  • 發表數: 15771
[轉貼]Unity – High Level Networking Concepts 翻譯

原文: High Level Networking Concepts
※這篇都是字,有些可能會翻得有點鬼打牆( ´ー`)フゥー...

這個章節涵蓋了使用 Unity 的網路系統製作遊戲前,需要理解的概念。

What is Networking?

Networking 是在兩個以上的電腦間做做通訊。基本概念就是 Client 端(要求資訊的電腦)與 Server 端(回應要求的電腦)之間的關係。Server 端可以是一台專用的 host 機器,或者簡單地讓某個玩家(Client)同時作為其他玩家的 host。決定好 Server 端之後, client 會連結到它,兩台電腦就可以交換遊戲中需要的資料了。

創建一個 network 遊戲需要大量的注意力去定義細節。即使在 Unity 裡面使用/創建 network 是很簡單的,但 network 仍然是相當複雜。在 Unity 設計讓它盡可能可靠且具有彈性。This means that you, as the game creator, are responsible for things that might be handled in an automatic but less robust way in other engines.你做出的選擇對遊戲的設計上會有很大的影響,所以最好在製作初期就完成它們。理解 networking 概念會幫助你計畫你的遊戲設計做得更好,並且避免在執行時出狀況。

Networking Approaches


這裡有兩個方式來架構 network 遊戲稱為 "Authoritative Server" 和 "Non-Authoritative Server"。兩者都需要依賴 server 連結 client 並在它們之間傳送資料。同時也都提供隱私,在 end users 並不會直接的與其他人連結,或是將IP顯露給其他人。

Authoritative Server

這個 Authoritative Server 的方式需要 server 去負責所有世界的模擬、遊戲規則和處理其他 client 端的操作。每個 client 傳送它們的操作 (以 keystrokes 的形式或是請求行為)給 server 端,並且不斷地從 server 接收遊戲目前的狀態。Client 永遠不會自行讓遊戲狀態做出任何改變。作為替代,它會告訴 server 它想要做甚麼,然後 server 會處理要求,並回傳給 client 這項要求最後的結果。

基本上,玩家想要做甚麼和實際上發生了甚麼,這兩個中間是會格一層的。這會讓 server 在決定如何更新遊戲狀態之前,先去傾聽所有 client 的要求。

這個方法的優點就是讓 client 端的作弊更加困難。例如 client 不可能靠著告訴 server (and thereby other clients) 來作弊的殺掉敵人,因為他們沒有得到自己做決定的權利它們只可以告訴 server 有一把武器開火以及從哪來,然後讓 server 決定是否造成擊殺。

其他 authoritative server 的範例會讓多人遊戲依賴物理系統。如果讓每個 client 去執行自己的物理模擬,就會在所有 client 之間在同步上產生誤差。但是如果所有的物理物件是在中央 server 處理以及更新狀態,並發送回給所有的 client 端,來確保他們的狀態是一致的。


Authoritative servers 的一個潛在的缺點是訊息在 network 之間移動時會帶著一個 time。如果玩家按下了一個操作往前移動然後它花費了十分之一秒去和 server 交談,這個延遲就會被玩家查覺。一個解決的方案是使用所謂的 "client-side prediction(客戶端預測)"。這個技術的本質是讓 client 可以依照本機的版本更新,但它應該在需要的時候,可以接收來自 server 的更正。通常這應該只會被使用在單純的遊戲動作,而非會明顯遊戲狀態的邏輯改變。例如,不智地讓玩家回報某個敵人被擊殺的這種應該讓 server 決定的事情。

因為 client-side prediction 是一個進階的課題,我們並沒打算在這個指南裡面有太多著墨,但如果你想要知道,在書上或網路上有很多相關資訊。

比起 non-authoritative server,authoritative 會需要更大的處裡開銷。當 server 不需要去處理所有遊戲狀態的改變時,可以把大量的處理分散到 client 裡。

Non-Authoritative Server

Non-Authoritative server 不會去控制所有使用者的操作結果。玩家會各自處理操作和遊戲邏輯,接著傳送所有被決定好的動作給 server。Server 接著與世界狀態同步所有動作。從設計角度來看,這是很容易去實作的。當 server 只是所有 client 之間訊息傳遞的中繼點,而不用去額外處理 client 的操作。

在 client 處理所有物理和事件以及告訴 server 發生了什麼事情時,不需要任何類似" prediction(預測)"的方法。因為他們都是自己物件的" owners",也是整個 network 之中唯一能修改本機物件的代理人。

Methods of Network Communication

現在我們結束了基本的網路遊戲架構,現在開始探索更低階,關於 client 和 server 如何互相溝通。


這裡有兩個相關的方法:Remote Procedure CallsState Synchronization。在任何遊戲中不難看到這兩個方法。

Remote Procedure Calls

Remote Procedure Calls (RPCs)都是用來跨網路觸發其他電腦的功能,雖然當 server 和 client 兩者都在同一台電腦上執行時,"network"就只是代表一個訊息的管道。Client 可以發送 RPCs 到 server,然後 server 可以傳送 RPCs 到一個或多個 clients。這大多是用在該動作很少發生時。例如,當一個 client 翻動一個開關去打開門,這個就可以傳送一個 PRC 到 server,告訴它門被開啟了。Server 可以接著傳送其他 RPC 給所有 clients,觸發它們本機的功能去打開相同的門。它們用來管理和執行個別的事件。

State Synchronization

State Synchronization 用來共享不斷變化的資料。最好的範例就是玩家在動作遊戲中的位置。玩家總是會移動、跑動、跳躍等等。在網路內的其他玩家,即使該角色不是在本機控制的,但仍然需要去知道它在哪裡以及正在做甚麼事情。透過不斷發送玩家位置的資料,遊戲可以正確的顯示其他玩家的位置。

這類需要定期和頻繁在網路內傳送的資料。因為這個資料是 time-sensitive(對時間敏感的),然後它需要時間透過網入在機器間移動,近可能縮減傳送所需的資料就變得非常重要。簡單來說,狀態同步化通常都需要大量的頻寬,所以你需要盡可能減少頻寬的使用。

Connecting servers and clients together

連結 server 和 clients 可以是一個複雜的處理。機器可有私密和公開的 IP 位置,然後他們可以有本機或外在的防火牆阻擾存取。Unity networking 旨在盡可能處理各種狀況,但那並沒有一個萬用的方案。

私密位址是指IP無法直接從網路存取(它們也稱作"
Network Address Translation or NAT" 位址,後面的方法會使用道它們)。簡單來說,私密位址會通過一個在 local router,它會將這個位址轉換成公開位址。經過這個動作,許多機器就可以使用私密位址透過一個單獨的公開 IP 位址和網路溝通。這是個很好的架構,直到在網路上的某地某人想要開始和一個私有位址連結之時。通訊通常是透過 router 的公開位址發生的,然後它應該要傳送資訊給私密位址。一個叫做"NAT punchthrough"的技術,使用了一個共享的 server 稱之為" facilitator(調停者)"來處理通訊,這樣私密位址也就可以從公開位址被搜尋到了。這個方式是先讓私密位址連結到 facilitator,然後透過 local router "punches(鑽)"出一個孔。這時 facilitator 就可以看到私密位址的公開 IP 以其它使用的 port,網路上的其他電腦這時也可以開始直接和這個無法被搜尋到的私密位址連結。(在實際操作中可以注意到更多 NAT punchthrough 的細節。)

公開位址就很直接了。在這裡主要的問題是連結會被內部或外部防火牆阻擋(內部防火牆是一個在本地執行用來防護的東西)。對於內部防火牆,使用者會被要求解除特定的 prot 讓遊戲 server 可以存取。相對之下,外部防火牆並不受使用者控制。Unity 會試圖去使用 NAT punchthrough 透過外部防火取得位址,但這個技術並不一定會成功。經測試,它通常都可以順利運作,但目前並沒有一個正式的研究報告上有這個部份的驗證。


剛才提及的連線問題對於 server 和 client 有不同的影響。Client 只需要請求離開 network 的管制,這是相對簡單的。如果 client 擁有一個公開位址就可以正常運作,因為只有在嚴格管制的企業網路才會被阻擋。如果 client 是私密位址,它可以連結至任何 server 除非該 server 是一個無法使用 NAT punchthrough 的私密位址(我們會在後面說明)。至於 server 端則較為複雜,因為 server 需要接收來自不明來源的連結。如果是公開位址,server 需要為遊戲開啟一個給網路存取的 port(也就是不被防火牆阻擋)。不然它就無法接收任何 client 的連結,也就無法使用了。如果 server 是私密位址,那它必須可以執行 NAT punchthrough 去允許連結並允許 NAT punchthrough 的穿透好讓 client 連結它。

Unity 提供工具去測試所有不同的連結狀況。當它被建立時就可以作連結。它會引發兩種狀況: 直接連結 ( client 需要知道 server 的 IP 位址或是 DNS 名稱)以及透過 Master Server 連結。Master Server 可以讓 client 知道 server 們的存在,這個必須是在不知道該 server 任何資訊時才會使用。

Minimizing Network Bandwidth


在複數 client 之間運作 State Synchronization,你不必同步每一個細節來顯示物件的同步。例如,當你同步一個角色時,你只需要在 client 之間發送它的位置和旋轉角度。即使該極為複雜,可能包含一個極深的 Transform 階層,但這整個階層的資料並不需要全部被共享。在你的遊戲內有大量的資料都是靜止無變化的,client 應該只需要在一開始放置好它,而非同步它。使用 infrequent 或 one-time RPC 呼叫就足以應付大部分功能的運作。善用安裝在遊戲中的資料,盡可能讓 client 自己處理。例如,如你所知的所有 texture 和 mesh 物件,它們通常是不會改變的,所以它們永遠不必執行同步。這是一個簡單的範例,但是它應該可以讓你思考哪些資料是絕對需要在 client 之間共享的。這才是你需要共享的資料。

哪個是需要被共享,而哪些是不需要的,這個工作可說是最困難的。特別是你沒有任何網路遊戲的製作經驗時。牢記,你可以把一個 PRC 發送到指定名稱的 level,讓所有 client 去讀取整個指定的 level 並加入它們自己的網路元件裡。盡可能架構讓 client 自給自足的運作系統進而減少頻寬的使用。

Multiplayer Game Performance

物理上的位置和 server 本身的效能會大大影響遊戲在它上面執行時的可玩性。位於遠離 server 大陸上的 client 可能就會感受到嚴重的延遲(lag)。這是網路在物理上的限制,唯一一個實際的解法就是盡可能讓 server 和使用它的 client 靠近,或是至少讓他們在同一個大陸上。

Extra Resources

我們蒐集了關於網路的資源連結:

Page last updated: 2011-11-18


原文出處:[譯]Unity – High Level Networking Concepts | NaCl's Blog
前一個主題 | 下一個主題 | 頁首 | | |



Powered by XOOPS 2.0 © 2001-2008 The XOOPS Project|