Unity網路篇:Unet同步問題整合。
首先要認清一點,Unet是伺服器權威的。這在同步問題中很是重要。
狀態同步是從伺服器向客戶端方向上的。本地客戶端沒有序列化的資料,因為它和伺服器共享同一個場景。任何為本地客戶端序列化的資料都是多餘的。然而,SyncVar鉤子函式會被本地客戶端呼叫。注意資料不會從客戶端向伺服器同步,這個方向上的操作叫做命令(Commands)。
除了可以直接用的network類的同步元件,我們還應該認識幾個操作:
同步變數[SyncVar]--
同步變數是NetworkBehaviour指令碼中的成員變數,他們會從伺服器同步到客戶端上。當一個物體被派生出來之後,或者一個新的玩家中途加入遊戲後,他會接收到他的視野內所有物體的同步變數。成員變數通過[SyncVar]標籤被配置成同步變數:
class Player :NetworkBehaviour
{
[SyncVar]
int health;
public void TakeDamage(int amount)
{
if (!isServer)
return;
health -= amount;
}
}
同步變數的狀態在OnStartClient()之前就被應用到物體上了,所以在OnStartClient中,物體的狀態已經是最新的資料。
同步變數可以是基礎型別,如整數,字串和浮點數。也可以是Unity內建資料型別,如Vector3和使用者自定義的結構體,但是對結構體型別的同步變數,如果只有幾個欄位的數值有變化,整個結構體都會被髮送。每個NetworkBehaviour指令碼可以有最多32個同步變數,包括同步列表(見下面的解釋)。
當同步變數有變化時,會自動傳送他們的最新資料。不需要手工為同步變數設定任何的髒資料位。
注意在屬性設定函式中設定一個同步變數的值不會使他的髒資料被設定。如果這樣做的話,會得到一個編譯期的警告。因為同步變數使用他們自己內部的記錄髒資料狀態,在屬性設定函式中設定髒位會引起遞迴呼叫問題。
同步變數還可以指定函式,使用hook: 當伺服器改變了playerName的值,客戶端會呼叫OnMyName這個函式
[SyncVar(hook = "OnMyName")]
public string playerName = "";
public void OnMyName(string newName)
{
playerName = newName;
nameInput.text = playerName;
}
同步列表(SyncLists)--
同步列表類似於同步變數,但是他們是一些值的列表而不是單個值。同步列表和同步變數都包含在初始的狀態更新裡。同步列表不需要[SyncVar]屬性,他們是特殊的類。內建的基礎型別屬性列表有:
SyncListString
SyncListFloat
SyncListInt
SyncListUInt
SyncListBool
還有個SyncListStruct可以給使用者自定義的結構體用。從SyncListStruct派生出的結構體類可以包含基礎型別,陣列和通用Unity型別的成員變數,但是不能包含複雜的類和通用容器。
同步列表有一個叫做SyncListChanged的回撥,可以使客戶端能接收到列表中的資料改動的通知。這個回撥函式被呼叫時,會被通知到操作型別,和修改的變數索引。
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;
}
}
定製序列化函式--
通常在指令碼中使用同步變數就夠了,但是有時候也需要更復雜的序列化程式碼。NetworkBehaviour中的虛擬函式允許開發者定製自己的序列化函式,這些函式有:
public virtual boolOnSerialize(NetworkWriter writer, bool initialState);
public virtual voidOnDeSerialize(NetworkReader reader, bool initialState);
initalState可以用來是第一次序列化資料還是隻傳送增量的資料。如果是第一次傳送給客戶端,必須要包含所有狀態的資料,後續的更新只需要包含增量的修改,以節省頻寬。同步變數的鉤子函式在initialState為True的時候不會被呼叫,而只會在增量更新函式中被呼叫。
如果一個類裡面了同步變數,這些的實現會自動被加到類裡面,因此一個有同步變數的類不能擁有自己的序列化函式。
OnSerialize函式應該返回True來指示有更新需要傳送,如果它返回了true,這個類的所有髒位都會被清除,如果它返回False,則髒標誌位不會被修改。這可以允許將多次改動合併在一起傳送,而不需要每一幀都發送。
序列化流程--
具有NetworkIdentity元件的物體可以帶有多個從NetworkBehaviour派生出來的指令碼,這些物體的序列化流程為:
在伺服器上:
- 每個NetworkBehaviour上都有一個髒資料掩碼,這個掩碼可以在OnSerialize函式中通過syncVarDirtyBits訪問到
- NetworkBehavious中的每個同步變數被指定了髒資料掩碼中的一位
- 對同步變數的修改會使對應的髒資料位被設定
- 或者可以通過呼叫SetDirtyBit直接修改髒資料位
- 的每個Update呼叫都會檢查他的NetworkIdentity元件
- 如果有標記為髒的NetworkBehaviour,就會為那個物體建立一個更新資料包
- 每個NetworkBehaviour元件的OnSerialize都被呼叫,來構建這個更新資料包
- 沒有髒資料位設定的NetworkBehaviour在資料包中新增0標誌
- 有髒資料位設定的NetworkBehavious寫入他們的髒資料和有改動的同步變數的值
- 如果一個NetworkBehavious的OnSerialize函式返回了True,那麼他的髒位被重置,因此直到下一次資料修改之前不會被再次傳送
- 更新資料包被髮送到能看見這個物體的所有客戶端
在客戶端:
- 接收到一個物體的更新資料包
- 每個NetworkBehavious指令碼的OnDeserialize被呼叫
- 這個物體上的每個NetworkBehavious指令碼讀取髒資料
- 如果關聯到這個NetworkBehaviour指令碼的髒資料位是0,OnDeserialize函式直接返回;
- 如果髒資料不是0,OnDeserialize函式繼續讀取後續的同步變數
- 如果有同步變數的鉤子,呼叫鉤子函式
對下面的程式碼:
public class data :NetworkBehaviour
{
[SyncVar]
public int int1 = 66;
[SyncVar]
public int int2 = 23487;
[SyncVar]
public string MyString = "esfdsagsdfgsdgdsfg";
}
產生的序列化OnSerialize將如下所示:
public override boolOnSerialize(NetworkWriter writer, bool forceAll)
{
if (forceAll)
{
// 第一次傳送物體資訊給客戶端,傳送全部資料
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;
}
反序列化將如下:
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();
}
}
如果這個NetworkBehaviour的基類也有一個序列化,基類的序列化函式也將被呼叫。
注意更新資料包可能會在緩衝區中合併,所以一個傳輸層資料包可能包含多個物體的更新資料包。
遠端動作--
網路系統允許在網路上執行遠端的動作。這類動作有時也叫做遠端過程呼叫(RPC)。有兩種型別的遠端過程呼叫,命令(Commands) – 由客戶端發起,執行在伺服器上;和客戶端遠端過程呼叫(ClientRpc) - 伺服器發起,執行在客戶端上。
命令(Commands)--
命令從客戶端上的物體發給上的物體。出於安全考慮,命令只能從控制的物體上發出,因此不能控制其他玩家的物體。要把一個函式變成命令,需要給這個函式新增[Command]屬性,並且為函式名新增“Cmd”字首,這樣這個函式會在客戶端上被呼叫時在上執行。所有的引數會自動和命令一起傳送給伺服器。
命令的名字必須要有“Cmd”字首。在閱讀程式碼的時候,這也是個提示 – 這個比較特殊,他不像普通函式一樣在本地被執行。
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);
}
}
}
注意如果每一幀都發送命令訊息,會產生很多的網路流量。
預設情況下,命令是通過0號通道(預設的可靠傳輸通道)進行傳輸的。所以預設情況下,所有的命令都會被可靠地傳送到。可以使用命令的“Channel”引數修改這個配置。引數是一個整數,表示通道號。
1號通道是預設的不可靠傳輸通道,如果要用這個通道,把這個引數設定為1,示例如下:
[Command(channel=1)]
從Unity5.2開始,可以從擁有客戶端授權的非物體發出命令。這些物體必須是使用函式NetworkServer.SpawnWithClientAuthority()派生出來的,或者是使用NetworkIdentity.AssignClientAuthority()授權過的。從物體傳送出來的命令會在伺服器上執行,而不是在相關物體所在的客戶端上。
客戶端遠端過程呼叫(ClientRPC Calls)
客戶端遠端過程呼叫從的物體傳送到客戶端的物體上去。他們可以從任何帶有NetworkIdentity並被派生出來的物體上發出。因為擁有授權,所以這個過程不存在安全問題。要把一個函式變成客戶端遠端過程呼叫,需要給函式新增[ClientRpc]屬性,並且為函式名新增“Rpc”字首。這個函式將在服務端上被呼叫時,在客戶端上執行。所有的引數都將自動傳給客戶端。
客戶端遠端呼叫必須帶有“Rpc”字首。在閱讀程式碼的時候,這將是個提示 – 這個比較特殊,不像一般那樣在本地執行。
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);
}
}
當使用伺服器模式執行的時候,客戶端遠端呼叫將在本地客戶端執行 – 即使他其實和執行在同一個程序。因此本地客戶端和遠端客戶端對客戶端遠端過程呼叫的處理是一樣的。
如果想將[ClientRpc]用在點選事件的同步操作上,不能直接繫結點選事件函式,而是應該起一個新的Rpc函式,點選事件去繫結這個Rpc函式,Rpc函式裡才是對點選事件的操作: //點選事件
public void ClickDXView()
{
RpcDXView();
}
[ClientRpc]
public void RpcDXView()
{
readyPN.gameObject.SetActive(false);
startGm();
Camera.main.GetComponent<DOTweenPath>().DOPlay();
}
回撥函式--
[ServerCallback]:只執行在伺服器端,並使一些特殊函式(eg:Update)不報錯(若在此函式中改變了帶有syncvar的變數,客戶端不同步)
(使用ServerCallback時,將Update中的重要語句摘出來寫入Rpc函式中並呼叫)
[ClientCallback]:只執行在客戶端
另:[Server]:只執行在伺服器端但是不能標識一些特殊函式(可以在這裡呼叫Rpc類函式)
遠端過程的引數
傳遞給客戶端遠端過程呼叫的引數會被序列化並在網路上傳送,這些引數可以是:
- 基本資料型別(位元組,整數,浮點樹,字串,64位無符號整數等)
- 基本資料型別的陣列
- 包含允許的資料型別的結構體
- Unity內建的數學型別(Vector3,Quaternion等)
- NetworkIdentity
- NetworkInstanceId
- NetworkHash128
- 帶有NetworkIdentity元件的物體
遠端過程的引數不可以是物體的子元件,像指令碼物件或Transform,他們也不能是其他不能在網路上被序列化的資料型別。
在使用過程中發現一個問題:帶有NetworkIdentity的元件在執行之前不能是隱藏的,否則同步會受影響,在程式碼Start函式中置為SetActive = false,或者因為網路問題一開始隱藏的物體在後續同步中都沒有問題。