状态同步

状态同步从服务器完成到远程客户端。本地客户端没有序列化到它的数据,因为它与服务器共享场景。序列化到本地客户端的任何数据都是多余的。但SyncVar回调在本地客户端上调用。

数据不会从远程客户端同步到服务器。这是Commands的工作。

SyncVars

SyncVars是从服务器同步到客户端的NetworkBehaviour脚本的成员变量。当一个对象产生时,或者一个新玩家加入正在进行的游戏时,它们将被发送到它们可见的网络对象上的所有SyncVars的最新状态。通过使用[SyncVar]自定义属性将成员变量制作为SyncVars

  1. class Player : NetworkBehaviour
  2. {
  3. [SyncVar]
  4. int health;
  5. public void TakeDamage(int amount)
  6. {
  7. if (!isServer)
  8. return;
  9. health -= amount;
  10. }
  11. }

在调用OnStartClient()之前,将SyncVars的状态应用于客户端上的对象,因此保证OnStartClient()内的对象状态为最新状态。

SyncVars可以是基本类型,例如整数,字符串和浮点数。它们也可以是Unity类型,如Vector3和用户定义的结构,但是对于结构SyncVars的更新将作为单片更新发送,而不是结构内的字段更改时的增量更改。一个NetworkBehaviour脚本最多可以有32个SyncVars - 这包括SyncLists

SyncVar的值发生变化时,服务器会自动发送SycnVar更新。SyncVars不需要手动弄脏字段。

SyncLists

SyncListsSyncVars类似,但它们是值列表而不是单个值。SyncList内容包含在使用SyncVar状态的初始状态更新中。SyncLists不需要SyncVar属性,它们是特定的类。有基本类型的内置SyncList类型:

  1. SyncListString
  2. SyncListFloat
  3. SyncListInt
  4. SyncListUInt
  5. SyncListBool

还有SyncListStruct可用于用户定义的结构列表。使用SyncListStruct派生类的结构可以包含基本类型,数组和常见Unity类型的成员。它们不能包含复杂的类或通用容器。

SyncLists有一个名为Callback的SyncListChanged委托,允许在列表内容发生更改时通知客户端。这个委托被调用,发生的操作类型和操作的项目索引。

  1. public class MyScript : NetworkBehaviour
  2. {
  3. public struct Buf
  4. {
  5. public int id;
  6. public string name;
  7. public float timer;
  8. }
  9. public class TestBufs : SyncListStruct<Buf> {}
  10. TestBufs m_bufs = new TestBufs();
  11. void BufChanged(SyncListStruct<Buf>.Operation op, int itemIndex)
  12. {
  13. Debug.Log("buf changed:" + op);
  14. }
  15. void Start()
  16. {
  17. m_bufs.Callback = BufChanged;
  18. }
  19. }

自定义序列化函数

通常,SyncVars的使用足以让脚本将其状态序列化到客户端,但有些情况下需要更复杂的序列化代码。用于SyncVar序列化的NetworkBehaviour上的虚拟功能可能会被开发人员用来执行自己的自定义序列化。这些功能是:

  1. public virtual bool OnSerialize(NetworkWriter writer, bool initialState);
  2. public virtual void OnDeSerialize(NetworkReader reader, bool initialState);

initialState标志可用于区分第一次对象序列化和何时发送增量更新。第一次将对象发送到客户端时,它必须包含完整的状态快照,但后续更新可以通过仅包含增量更改来节省带宽。请注意,当initialStatetrue时,不会调用SyncVar回调函数,仅用于增量更新。

如果一个类有SyncVars,那么这些函数的实现会自动添加到类中。所以具有SyncVars的类不能具有自定义序列化函数。

OnSerialize函数应返回true以指示应发送更新。如果它返回true,那么该脚本的脏位将设置为零,如果它返回false,则脏位不会更改。这允许对脚本进行多次更改并随时间累积,并在系统准备好时发送,而不是每个帧。

序列化流程

具有NetworkIdentity组件的游戏对象可以有多个从NetworkBehaviour派生的脚本。序列化这些对象的流程是:

在服务器上:

  1. 每个NetworkBehaviour都有一个脏屏蔽。此掩码在OnSerialize中可用作syncVarDirtyBits
  2. NetworkBehaviour脚本中的每个SyncVar在脏屏蔽中被分配一个位。
  3. 更改SyncVars的值会导致该SyncVar的位在脏屏蔽中设置
  4. 或者,调用SetDirtyBit()直接写入脏屏蔽
  5. NetworkIdentity对象作为其更新循环的一部分在服务器上进行检查
  6. 如果NetworkIdentity上的任何NetworkBehaviours都是脏的,则会为该对象创建一个UpdateVars数据包
  7. UpdateVars数据包通过在对象上的每个NetworkBehaviour上调用OnSerialize来填充
  8. 没有脏的NetworkBehaviours会为数据包的脏位写零
  9. 肮脏的NetworkBehaviours会写入其肮脏的掩码,然后是已更改的SyncVars的值
  10. 如果对于NetworkBehaviour而言,OnSerialize返回true,则将为该NetworkBehaviour重置脏掩码,因此它将不会再次发送,直到其值发生更改。
  11. UpdateVars数据包发送到正在观察该对象的就绪客户端

在客户端:

  1. 接收到一个对象的UpdateVars数据包
  2. OnDeserialize函数为对象上的每个NetworkBehaviour脚本调用
  3. 对象上的每个NetworkBehaviour脚本都会读取一个脏屏蔽。
  4. 如果NetworkBehaviour的脏屏蔽为零,则OnDeserialize函数将不会再读取而返回
  5. 如果脏掩码为非零值,则OnDeserialize函数将读取与设置的脏位相对应的SyncVars的值
  6. 如果存在SyncVar回调函数,则使用从流中读取的值调用这些函数。

所以对于这个脚本:

  1. public class data : NetworkBehaviour
  2. {
  3. [SyncVar]
  4. public int int1 = 66;
  5. [SyncVar]
  6. public int int2 = 23487;
  7. [SyncVar]
  8. public string MyString = "esfdsagsdfgsdgdsfg";
  9. }

生成的OnSerialize函数如下所示:

  1. public override bool OnSerialize(NetworkWriter writer, bool forceAll)
  2. {
  3. if (forceAll)
  4. {
  5. // the first time an object is sent to a client, send all the data (and no dirty bits)
  6. writer.WritePackedUInt32((uint)this.int1);
  7. writer.WritePackedUInt32((uint)this.int2);
  8. writer.Write(this.MyString);
  9. return true;
  10. }
  11. bool wroteSyncVar = false;
  12. if ((base.get_syncVarDirtyBits() & 1u) != 0u)
  13. {
  14. if (!wroteSyncVar)
  15. {
  16. // write dirty bits if this is the first SyncVar written
  17. writer.WritePackedUInt32(base.get_syncVarDirtyBits());
  18. wroteSyncVar = true;
  19. }
  20. writer.WritePackedUInt32((uint)this.int1);
  21. }
  22. if ((base.get_syncVarDirtyBits() & 2u) != 0u)
  23. {
  24. if (!wroteSyncVar)
  25. {
  26. // write dirty bits if this is the first SyncVar written
  27. writer.WritePackedUInt32(base.get_syncVarDirtyBits());
  28. wroteSyncVar = true;
  29. }
  30. writer.WritePackedUInt32((uint)this.int2);
  31. }
  32. if ((base.get_syncVarDirtyBits() & 4u) != 0u)
  33. {
  34. if (!wroteSyncVar)
  35. {
  36. // write dirty bits if this is the first SyncVar written
  37. writer.WritePackedUInt32(base.get_syncVarDirtyBits());
  38. wroteSyncVar = true;
  39. }
  40. writer.Write(this.MyString);
  41. }
  42. if (!wroteSyncVar)
  43. {
  44. // write zero dirty bits if no SyncVars were written
  45. writer.WritePackedUInt32(0);
  46. }
  47. return wroteSyncVar;
  48. }

OnDeserialize函数是这样的:

  1. public override void OnDeserialize(NetworkReader reader, bool initialState)
  2. {
  3. if (initialState)
  4. {
  5. this.int1 = (int)reader.ReadPackedUInt32();
  6. this.int2 = (int)reader.ReadPackedUInt32();
  7. this.MyString = reader.ReadString();
  8. return;
  9. }
  10. int num = (int)reader.ReadPackedUInt32();
  11. if ((num & 1) != 0)
  12. {
  13. this.int1 = (int)reader.ReadPackedUInt32();
  14. }
  15. if ((num & 2) != 0)
  16. {
  17. this.int2 = (int)reader.ReadPackedUInt32();
  18. }
  19. if ((num & 4) != 0)
  20. {
  21. this.MyString = reader.ReadString();
  22. }
  23. }

如果NetworkBehaviour也具有序列化函数的基类,则还应该调用基类函数。

请注意,为对象状态更新创建的UpdateVar数据包在发送到客户端之前可能会在缓冲区中聚合,因此单个传输层数据包可能包含多个对象的更新。

?