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

Google 自訂搜尋

Goole 廣告

隨機相片
IMG_2329128.jpg

授權條款

使用者登入
使用者名稱:

密碼:


忘了密碼?

現在就註冊!

對這文章發表回應

發表限制: 非會員 可以發表

發表者: 冷日 發表時間: 2014/11/26 6:29:24

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
內容圖示
url email imgsrc image code quote
樣本
bold italic underline linethrough   












 [詳情...]
validation picture

注意事項:
預覽不需輸入認證碼,僅真正發送文章時才會檢查驗證碼。
認證碼有效期10分鐘,若輸入資料超過10分鐘,請您備份內容後,重新整理本頁並貼回您的內容,再輸入驗證碼送出。

選項

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