首先要認清一點,Unet是服務器權威的。這在同步問題中很是重要。
狀態同步是從服務器向客戶端方向上的。本地客戶端沒有序列化的數據,因爲它和服務器共享同一個場景。任何爲本地客戶端序列化的數據都是多餘的。然而,SyncVar的hook函數會被本地客戶端調用。注意數據不會從客戶端向服務器同步,這個方向上的操作叫做命令(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可以用來標識是第一次序列化數據還是隻發送增量的數據。如果是第一次發送給客戶端,必須要包含所有狀態的數據,後續的更新只需要包含增量的修改,以節省帶寬。同步變量的hook函數在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函數繼續讀取後續的同步變量
- 如果有同步變量的hook函數,調用hook函數
對下面的代碼:
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,或者因爲網絡問題一開始隱藏的物體在後續同步中都沒有問題
原文: