• 状态同步
    • SyncVars
    • SyncLists
  • 自定义序列化函数
  • 序列化流程

    状态同步

    状态同步从服务器完成到远程客户端。本地客户端没有序列化到它的数据,因为它与服务器共享场景。序列化到本地客户端的任何数据都是多余的。但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数据包在发送到客户端之前可能会在缓冲区中聚合,因此单个传输层数据包可能包含多个对象的更新。

    ?