Unet基础学习
轉(zhuǎn)載自:http://www.cnblogs.com/xianunity/p/5643469.html
包含
[SyncVar]
[Command]
[ClientCallback]
[SeverCallback]等
?
Unet針對(duì)不同用戶的使用有高級(jí)和低級(jí)API兩種--用Unity制作多玩家游戲的用戶,這類用戶應(yīng)該從NetworkManager或者高級(jí)API開始。
搭建網(wǎng)絡(luò)架構(gòu)或制作高級(jí)的多玩家游戲的用戶,這類用戶應(yīng)該從網(wǎng)絡(luò)傳輸層低級(jí)API開始。
下面從高級(jí)API(HLAPI)開始介紹,使用高級(jí)API可以實(shí)現(xiàn):
- 通過(guò)Network Manager控制游戲的網(wǎng)絡(luò)狀態(tài)
- 操作客戶端主持的游戲,主持游戲的客戶端同時(shí)也是一個(gè)玩家
- 使用一個(gè)通用的序列化器序列化數(shù)據(jù)
- 發(fā)送和接收網(wǎng)絡(luò)消息
- 從客戶端向服務(wù)器發(fā)送網(wǎng)絡(luò)命令
- 在服務(wù)器上遠(yuǎn)程調(diào)用客戶端提供的過(guò)程(RPC)
- 從服務(wù)器向客戶端發(fā)送網(wǎng)絡(luò)事件
關(guān)于Unet的介紹網(wǎng)上有很多,大家可以自行查找。
?
首先,Unet提供了網(wǎng)絡(luò)連接以及管理的組件:
網(wǎng)絡(luò)管理器(NetworkManager)可以作為控制多人游戲的核心組件。在場(chǎng)景中創(chuàng)建一個(gè)空的游戲?qū)ο蠡蛱暨x一個(gè)方便管理器對(duì)象。從Network/NetworkManager菜單項(xiàng)里選擇添加NetworkManager組件。新添加的NetworkManager應(yīng)如圖所示:
在編輯器中的NetworkManager的的inspector 面板上,有允許您配置和控制與網(wǎng)絡(luò)相關(guān)的很多東西。
NetworkManagerHUD 是與NetworkManager相關(guān)的的另一個(gè)組件。游戲運(yùn)行控制網(wǎng)絡(luò)狀態(tài)時(shí),它給你一個(gè)簡(jiǎn)單的用戶界面。用來(lái)調(diào)試或者實(shí)驗(yàn)都可以,但不能用作游戲最終的 ui 設(shè)計(jì)。NetworkManagerHUD 看起來(lái)像:
真正的游戲用戶需要自己設(shè)置正確的UI ,用于控制游戲狀態(tài)和允許玩家選擇什么樣的游戲 。用戶設(shè)置自己的UI時(shí),腳本需要繼承NetworkManager,使用里面的StartHost(),StartClient()來(lái)開啟伺服器和客戶端。并且可以重寫方法來(lái)檢測(cè)連接或者實(shí)現(xiàn)相應(yīng)功能,eg:OnClientConnect,OnStartHost等。
之后便可以使用一系列組件實(shí)現(xiàn)自己想要的功能了:
unity自帶的組件有上述這些,根據(jù)名字也可以大致推測(cè)出可以實(shí)現(xiàn)什么功能,下面簡(jiǎn)要介紹一下:
--Animator:動(dòng)畫同步
--Identity:網(wǎng)絡(luò)標(biāo)識(shí)組件(帶有Identity的物體只有網(wǎng)絡(luò)開啟后才會(huì)顯示出來(lái))
--LobbyManager:游戲大廳
--LobbyPlayer:玩家控制
--Manager:網(wǎng)絡(luò)管理器
--ManagerHUD:默認(rèn)UI
--StartPosition:游戲開始玩家所處的位置
--Tranform:物體位移等的同步控制(掛載在需要同步物體的最小父物體上即可)
?
?
?
同步中的操作
?
同步變量[SyncVar]--
同步變量是NetworkBehaviour腳本中的成員變量,他們會(huì)從服務(wù)器同步到客戶端上。當(dāng)一個(gè)物體被派生出來(lái)之后,或者一個(gè)新的玩家中途加入游戲后,他會(huì)接收到他的視野內(nèi)所有物體的同步變量。成員變量通過(guò)[SyncVar]標(biāo)簽被配置成同步變量:
class Player :NetworkBehaviour
{
[SyncVar]
int health;
public void TakeDamage(int amount)
{
if (!isServer)
return;
health -= amount;
}
}
同步變量的狀態(tài)在OnStartClient()之前就被應(yīng)用到物體上了,所以在OnStartClient函數(shù)中,物體的狀態(tài)已經(jīng)是最新的數(shù)據(jù)。
同步變量可以是基礎(chǔ)類型,如整數(shù),字符串和浮點(diǎn)數(shù)。也可以是Unity內(nèi)置數(shù)據(jù)類型,如Vector3和用戶自定義的結(jié)構(gòu)體,但是對(duì)結(jié)構(gòu)體類型的同步變量,如果只有幾個(gè)字段的數(shù)值有變化,整個(gè)結(jié)構(gòu)體都會(huì)被發(fā)送。每個(gè)NetworkBehaviour腳本可以有最多32個(gè)同步變量,包括同步列表(見下面的解釋)。
當(dāng)同步變量有變化時(shí),服務(wù)器會(huì)自動(dòng)發(fā)送他們的最新數(shù)據(jù)。不需要手工為同步變量設(shè)置任何的臟數(shù)據(jù)標(biāo)志位。
注意在屬性設(shè)置函數(shù)中設(shè)置一個(gè)同步變量的值不會(huì)使他的臟數(shù)據(jù)標(biāo)志被設(shè)置。如果這樣做的話,會(huì)得到一個(gè)編譯期的警告。因?yàn)橥阶兞渴褂盟麄冏约簝?nèi)部的標(biāo)識(shí)記錄臟數(shù)據(jù)狀態(tài),在屬性設(shè)置函數(shù)中設(shè)置臟位會(huì)引起遞歸調(diào)用問(wèn)題。
同步變量還可以指定函數(shù),使用hook:
當(dāng)服務(wù)器改變了playerName的值,客戶端會(huì)調(diào)用OnMyName這個(gè)函數(shù)
[SyncVar(hook = "OnMyName")]
public string playerName = "";
public void OnMyName(string newName)
{
??????????? playerName = newName;
??????????? nameInput.text = playerName;
}
同步列表(SyncLists)--
同步列表類似于同步變量,但是他們是一些值的列表而不是單個(gè)值。同步列表和同步變量都包含在初始的狀態(tài)更新里。同步列表不需要[SyncVar]屬性標(biāo)識(shí),他們是特殊的類。內(nèi)建的基礎(chǔ)類型屬性列表有:
SyncListString
SyncListFloat
SyncListInt
SyncListUInt
SyncListBool
還有個(gè)SyncListStruct可以給用戶自定義的結(jié)構(gòu)體用。從SyncListStruct派生出的結(jié)構(gòu)體類可以包含基礎(chǔ)類型,數(shù)組和通用Unity類型的成員變量,但是不能包含復(fù)雜的類和通用容器。
同步列表有一個(gè)叫做SyncListChanged的回調(diào)函數(shù),可以使客戶端能接收到列表中的數(shù)據(jù)改動(dòng)的通知。這個(gè)回調(diào)函數(shù)被調(diào)用時(shí),會(huì)被通知到操作類型,和修改的變量索引。
public class MyScript :NetworkBehaviour
{
public struct Buf
{
public int id;
public string name;
public float timer;
};
public class TestBufs : SyncListStruct<Buf> {}
TestBufs m_bufs = new TestBufs();
void BufChanged(Operation op, int itemIndex)
{
Debug.Log("buf changed:" + op);
}
void Start()
{
m_bufs.Callback = BufChanged;
}
}
定制序列化函數(shù)--
通常在腳本中使用同步變量就夠了,但是有時(shí)候也需要更復(fù)雜的序列化代碼。NetworkBehaviour中的虛函數(shù)允許開發(fā)者定制自己的序列化函數(shù),這些函數(shù)有:
public virtual boolOnSerialize(NetworkWriter writer, bool initialState);
public virtual voidOnDeSerialize(NetworkReader reader, bool initialState);
initalState可以用來(lái)標(biāo)識(shí)是第一次序列化數(shù)據(jù)還是只發(fā)送增量的數(shù)據(jù)。如果是第一次發(fā)送給客戶端,必須要包含所有狀態(tài)的數(shù)據(jù),后續(xù)的更新只需要包含增量的修改,以節(jié)省帶寬。同步變量的鉤子函數(shù)在initialState為True的時(shí)候不會(huì)被調(diào)用,而只會(huì)在增量更新函數(shù)中被調(diào)用。
如果一個(gè)類里面聲明了同步變量,這些函數(shù)的實(shí)現(xiàn)會(huì)自動(dòng)被加到類里面,因此一個(gè)有同步變量的類不能擁有自己的序列化函數(shù)。
OnSerialize函數(shù)應(yīng)該返回True來(lái)指示有更新需要發(fā)送,如果它返回了true,這個(gè)類的所有臟標(biāo)志位都會(huì)被清除,如果它返回False,則臟標(biāo)志位不會(huì)被修改。這可以允許將多次改動(dòng)合并在一起發(fā)送,而不需要每一幀都發(fā)送。?
序列化流程--
具有NetworkIdentity組件的游戲物體可以帶有多個(gè)從NetworkBehaviour派生出來(lái)的腳本,這些物體的序列化流程為:
在服務(wù)器上:
- 每個(gè)NetworkBehaviour上都有一個(gè)臟數(shù)據(jù)掩碼,這個(gè)掩碼可以在OnSerialize函數(shù)中通過(guò)syncVarDirtyBits訪問(wèn)到
- NetworkBehavious中的每個(gè)同步變量被指定了臟數(shù)據(jù)掩碼中的一位
- 對(duì)同步變量的修改會(huì)使對(duì)應(yīng)的臟數(shù)據(jù)位被設(shè)置
- 或者可以通過(guò)調(diào)用SetDirtyBit函數(shù)直接修改臟數(shù)據(jù)標(biāo)志位
-?服務(wù)器的每個(gè)Update調(diào)用都會(huì)檢查他的NetworkIdentity組件
- 如果有標(biāo)記為臟的NetworkBehaviour,就會(huì)為那個(gè)物體創(chuàng)建一個(gè)更新數(shù)據(jù)包
- 每個(gè)NetworkBehaviour組件的OnSerialize函數(shù)都被調(diào)用,來(lái)構(gòu)建這個(gè)更新數(shù)據(jù)包
- 沒有臟數(shù)據(jù)位設(shè)置的NetworkBehaviour在數(shù)據(jù)包中添加0標(biāo)志
- 有臟數(shù)據(jù)位設(shè)置的NetworkBehavious寫入他們的臟數(shù)據(jù)和有改動(dòng)的同步變量的值
- 如果一個(gè)NetworkBehavious的OnSerialize函數(shù)返回了True,那么他的臟標(biāo)志位被重置,因此直到下一次數(shù)據(jù)修改之前不會(huì)被再次發(fā)送
- 更新數(shù)據(jù)包被發(fā)送到能看見這個(gè)物體的所有客戶端
在客戶端:
- 接收到一個(gè)物體的更新數(shù)據(jù)包
- 每個(gè)NetworkBehavious腳本的OnDeserialize函數(shù)被調(diào)用
- 這個(gè)物體上的每個(gè)NetworkBehavious腳本讀取臟數(shù)據(jù)標(biāo)識(shí)
- 如果關(guān)聯(lián)到這個(gè)NetworkBehaviour腳本的臟數(shù)據(jù)位是0,OnDeserialize函數(shù)直接返回;
- 如果臟數(shù)據(jù)標(biāo)志不是0,OnDeserialize函數(shù)繼續(xù)讀取后續(xù)的同步變量
- 如果有同步變量的鉤子函數(shù),調(diào)用鉤子函數(shù)
對(duì)下面的代碼:
public class data :NetworkBehaviour
{
[SyncVar]
public int int1 = 66;
[SyncVar]
public int int2 = 23487;
[SyncVar]
public string MyString = "esfdsagsdfgsdgdsfg";
}
產(chǎn)生的序列化函數(shù)OnSerialize將如下所示:
public override boolOnSerialize(NetworkWriter writer, bool forceAll)
{
if (forceAll)
{
// 第一次發(fā)送物體信息給客戶端,發(fā)送全部數(shù)據(jù)
writer.WritePackedUInt32((uint)this.int1);
writer.WritePackedUInt32((uint)this.int2);
writer.Write(this.MyString);
return true;
}
bool wroteSyncVar = false;
if ((base.get_syncVarDirtyBits() & 1u) != 0u)
{
if (!wroteSyncVar)
{
// write dirty bits if this is the first SyncVar written
writer.WritePackedUInt32(base.get_syncVarDirtyBits());
wroteSyncVar = true;
}
writer.WritePackedUInt32((uint)this.int1);
}
if ((base.get_syncVarDirtyBits() & 2u) != 0u)
{
if (!wroteSyncVar)
{
// write dirty bits if this is the first SyncVar written
writer.WritePackedUInt32(base.get_syncVarDirtyBits());
wroteSyncVar = true;
}
writer.WritePackedUInt32((uint)this.int2);
}
if ((base.get_syncVarDirtyBits() & 4u) != 0u)
{
if (!wroteSyncVar)
{
// write dirty bits if this is the first SyncVar written
writer.WritePackedUInt32(base.get_syncVarDirtyBits());
wroteSyncVar = true;
}
writer.Write(this.MyString);
}
if (!wroteSyncVar)
{
// write zero dirty bits if no SyncVars were written
writer.WritePackedUInt32(0);
}
return wroteSyncVar;
}
反序列化函數(shù)將如下:
public override voidOnDeserialize(NetworkReader reader, bool initialState)
{
if (initialState)
{
this.int1 = (int)reader.ReadPackedUInt32();
this.int2 = (int)reader.ReadPackedUInt32();
this.MyString = reader.ReadString();
return;
}
int num = (int)reader.ReadPackedUInt32();
if ((num & 1) != 0)
{
this.int1 = (int)reader.ReadPackedUInt32();
}
if ((num & 2) != 0)
{
this.int2 = (int)reader.ReadPackedUInt32();
}
if ((num & 4) != 0)
{
this.MyString = reader.ReadString();
}
}
如果這個(gè)NetworkBehaviour的基類也有一個(gè)序列化函數(shù),基類的序列化函數(shù)也將被調(diào)用。
注意更新數(shù)據(jù)包可能會(huì)在緩沖區(qū)中合并,所以一個(gè)傳輸層數(shù)據(jù)包可能包含多個(gè)物體的更新數(shù)據(jù)包。
遠(yuǎn)程動(dòng)作--
網(wǎng)絡(luò)系統(tǒng)允許在網(wǎng)絡(luò)上執(zhí)行遠(yuǎn)程的動(dòng)作。這類動(dòng)作有時(shí)也叫做遠(yuǎn)程過(guò)程調(diào)用(RPC)。有兩種類型的遠(yuǎn)程過(guò)程調(diào)用,命令(Commands) – 由客戶端發(fā)起,運(yùn)行在服務(wù)器上;和客戶端遠(yuǎn)程過(guò)程調(diào)用(ClientRpc) - 服務(wù)器發(fā)起,運(yùn)行在客戶端上。
命令(Commands)--
命令從客戶端上的物體發(fā)給服務(wù)器上的物體。出于安全考慮,命令只能從玩家控制的物體上發(fā)出,因此玩家不能控制其他玩家的物體。要把一個(gè)函數(shù)變成命令,需要給這個(gè)函數(shù)添加[Command]屬性,并且為函數(shù)名添加"Cmd"前綴,這樣這個(gè)函數(shù)會(huì)在客戶端上被調(diào)用時(shí)在服務(wù)器上運(yùn)行。所有的參數(shù)會(huì)自動(dòng)和命令一起發(fā)送給服務(wù)器。
命令函數(shù)的名字必須要有"Cmd"前綴。在閱讀代碼的時(shí)候,這也是個(gè)提示 – 這個(gè)函數(shù)比較特殊,他不像普通函數(shù)一樣在本地被執(zhí)行。
class Player :NetworkBehaviour
{
public GameObject bulletPrefab;
[Command]
void CmdDoFire(float lifeTime)
{
GameObject bullet =(GameObject)Instantiate(
bulletPrefab,
transform.position +transform.right,
Quaternion.identity);
var bullet2D =bullet.GetComponent<Rigidbody2D>();
bullet2D.velocity = transform.right *bulletSpeed;
Destroy(bullet, lifeTime);
NetworkServer.Spawn(bullet);
}
void Update()
{
if (!isLocalPlayer)
return;
if (Input.GetKeyDown(KeyCode.Space))
{
CmdDoFire(3.0f);
}
}
}
注意如果每一幀都發(fā)送命令消息,會(huì)產(chǎn)生很多的網(wǎng)絡(luò)流量。
默認(rèn)情況下,命令是通過(guò)0號(hào)通道(默認(rèn)的可靠傳輸通道)進(jìn)行傳輸?shù)摹K阅J(rèn)情況下,所有的命令都會(huì)被可靠地發(fā)送到服務(wù)器。可以使用命令的"Channel"參數(shù)修改這個(gè)配置。參數(shù)是一個(gè)整數(shù),表示通道號(hào)。
1號(hào)通道是默認(rèn)的不可靠傳輸通道,如果要用這個(gè)通道,把這個(gè)參數(shù)設(shè)置為1,示例如下:
[Command(channel=1)]
從Unity5.2開始,可以從擁有客戶端授權(quán)的非玩家物體發(fā)出命令。這些物體必須是使用函數(shù)NetworkServer.SpawnWithClientAuthority()派生出來(lái)的,或者是使用NetworkIdentity.AssignClientAuthority()授權(quán)過(guò)的。從物體發(fā)送出來(lái)的命令會(huì)在服務(wù)器上運(yùn)行,而不是在相關(guān)玩家物體所在的客戶端上。
客戶端遠(yuǎn)程過(guò)程調(diào)用(ClientRPC Calls)
客戶端遠(yuǎn)程過(guò)程調(diào)用從服務(wù)器的物體發(fā)送到客戶端的物體上去。他們可以從任何帶有NetworkIdentity并被派生出來(lái)的物體上發(fā)出。因?yàn)?span style="color:#075db3; text-decoration:underline">服務(wù)器擁有授權(quán),所以這個(gè)過(guò)程不存在安全問(wèn)題。要把一個(gè)函數(shù)變成客戶端遠(yuǎn)程過(guò)程調(diào)用,需要給函數(shù)添加[ClientRpc]屬性,并且為函數(shù)名添加"Rpc"前綴。這個(gè)函數(shù)將在服務(wù)端上被調(diào)用時(shí),在客戶端上執(zhí)行。所有的參數(shù)都將自動(dòng)傳給客戶端。
客戶端遠(yuǎn)程調(diào)用必須帶有"Rpc"前綴。在閱讀代碼的時(shí)候,這將是個(gè)提示 – 這個(gè)函數(shù)比較特殊,不像一般函數(shù)那樣在本地執(zhí)行。
class Player :NetworkBehaviour
{
[SyncVar]
int health;
[ClientRpc]
void RpcDamage(int amount)
{
Debug.Log("Took damage:" +amount);
}
public void TakeDamage(int amount)
{
if (!isServer)
return;
health -= amount;
RpcDamage(amount);
}
}
當(dāng)使用伺服器模式運(yùn)行游戲的時(shí)候,客戶端遠(yuǎn)程調(diào)用將在本地客戶端執(zhí)行 – 即使他其實(shí)和服務(wù)器運(yùn)行在同一個(gè)進(jìn)程。因此本地客戶端和遠(yuǎn)程客戶端對(duì)客戶端遠(yuǎn)程過(guò)程調(diào)用的處理是一樣的。
如果想將[ClientRpc]用在點(diǎn)擊事件的同步操作上,不能直接綁定點(diǎn)擊事件函數(shù),而是應(yīng)該起一個(gè)新的Rpc函數(shù),點(diǎn)擊事件去綁定這個(gè)Rpc函數(shù),Rpc函數(shù)里才是對(duì)點(diǎn)擊事件的操作:
?//點(diǎn)擊事件
??? public void ClickDXView()
??? {
??????????? RpcDXView();
??? }
??? [ClientRpc]
??? public void RpcDXView()
??? {
????????readyPN.gameObject.SetActive(false);
??????? startGm();
??????? Camera.main.GetComponent<DOTweenPath>().DOPlay();
??? }
回調(diào)函數(shù)--
[ServerCallback]:只執(zhí)行在服務(wù)器端,并使一些特殊函數(shù)(eg:Update)不報(bào)錯(cuò)(若在此函數(shù)中改變了帶有syncvar的變量,客戶端不同步)
(使用ServerCallback時(shí),將Update中的重要語(yǔ)句摘出來(lái)寫入Rpc函數(shù)中并調(diào)用)
[ClientCallback]:只執(zhí)行在客戶端
另:[Server]:只執(zhí)行在服務(wù)器端但是不能標(biāo)識(shí)一些特殊函數(shù)(可以在這里調(diào)用Rpc類函數(shù))
? ?
遠(yuǎn)程過(guò)程的參數(shù)
傳遞給客戶端遠(yuǎn)程過(guò)程調(diào)用的參數(shù)會(huì)被序列化并在網(wǎng)絡(luò)上傳送,這些參數(shù)可以是:
- 基本數(shù)據(jù)類型(字節(jié),整數(shù),浮點(diǎn)樹,字符串,64位無(wú)符號(hào)整數(shù)等)
- 基本數(shù)據(jù)類型的數(shù)組
- 包含允許的數(shù)據(jù)類型的結(jié)構(gòu)體
- Unity內(nèi)建的數(shù)學(xué)類型(Vector3,Quaternion等)
- NetworkIdentity
- NetworkInstanceId
- NetworkHash128
- 帶有NetworkIdentity組件的物體
遠(yuǎn)程過(guò)程的參數(shù)不可以是游戲物體的子組件,像腳本對(duì)象或Transform,他們也不能是其他不能在網(wǎng)絡(luò)上被序列化的數(shù)據(jù)類型。
?
在使用過(guò)程中發(fā)現(xiàn)一個(gè)問(wèn)題:帶有NetworkIdentity的組件在運(yùn)行之前不能是隱藏的,否則同步會(huì)受影響,在代碼Start函數(shù)中置為SetActive = false,或者因?yàn)榫W(wǎng)絡(luò)問(wèn)題一開始隱藏的物體在后續(xù)同步中都沒有問(wèn)題。
總結(jié)
- 上一篇: EventTrigger接管所有事件导致
- 下一篇: Unity3D粒子系统碰撞器抑制、反弹