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

Google 自訂搜尋

Goole 廣告

隨機相片
PIMG_00401.jpg

授權條款

使用者登入
使用者名稱:

密碼:


忘了密碼?

現在就註冊!

對這文章發表回應

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

發表者: 冷日 發表時間: 2014/11/27 9:06:24

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












 [詳情...]
validation picture

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

選項

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