英文原文:
https://mirror-networking.gitbook.io/docs/guides/synchronization
状态同步是指对属于脚本的整数、浮点数、字符串和布尔值等值进行同步。
状态同步是从服务器到远程客户端完成的。本地客户端没有序列化的数据。它不需要它,因为它与服务器共享场景。但是,在本地客户端上调用 SyncVar 挂钩。
数据不会以相反的方向同步 - 从远程客户端到服务器。为此,您需要使用命令。
-
SyncVars
SyncVars 是继承自 NetworkBehaviour 的脚本变量,从服务器同步到客户端。 -
SyncEvents (Obsolete)
SyncEvents 是类似于 ClientRpc 的网络事件,但它们不是在游戏对象上调用函数,而是触发事件。重要提示:在版本 18.0.0 中已删除,有关详细信息,请参阅此问题。 -
SyncLists
SyncLists 包含值列表并将数据从服务器同步到客户端。 -
SyncDictionary
SyncDictionary 是一个关联数组,其中包含键值对的无序列表。 -
SyncHashSet
一组不重复的无序值。 -
SyncSortedSet
一组排序的不重复的值。
同步到所有者
当您不希望其他玩家看到某些玩家数据时,通常会出现这种情况。在检查器中,将“网络同步模式”从“观察者”(默认)更改为“所有者”,让 Mirror 知道仅与拥有的客户端同步数据。
例如,假设您正在制作库存系统。假设玩家 A、B 和 C 在同一区域。全网共有12个对象:
- 客户端 A 有玩家 A(他自己)、玩家 B 和玩家 C
- 客户端 B 有玩家 A、玩家 B(他自己)和玩家 C
- 客户端 C 有玩家 A、玩家 B 和玩家 C(他自己)
- 服务器有玩家 A、玩家 B、玩家 C
他们每个人都有一个库存组件
假设玩家 A 捡到了一些战利品。服务器将战利品添加到玩家的 A 库存中,该库存将有一个 SyncLists of Items。
默认情况下,Mirror 现在必须在各处同步玩家 A 的库存,这意味着向客户端 A、客户端 B 和客户端 C 发送更新消息,因为他们都有一个玩家 A 的副本。这很浪费,客户端 B 和客户端 C 没有需要了解玩家 A 的库存,他们永远不会在屏幕上看到它。这也是一个安全问题,有人可以破解客户端并显示其他人的库存并利用它为自己谋利。
如果您将 Inventory 组件中的“Network Sync Mode”设置为“Owner”,则 Player A 的库存将仅与 Client A 同步。
现在,假设一个地区有 50 个人而不是 3 个人,其中一个人捡起战利品。这意味着您只需发送 1 条消息,而不是向 50 个不同的客户端发送 50 条消息。这会对您的游戏中的带宽产生很大影响。
其他典型用例包括任务、纸牌游戏中玩家的手牌、技能、经验或您不需要与其他玩家共享的任何其他数据。
高级状态同步
在大多数情况下,使用 SyncVars 足以让您的游戏脚本将其状态序列化给客户端。但是在某些情况下,您可能需要更复杂的序列化代码。此页面仅适用于需要超出 Mirror 正常 SyncVar 功能的自定义同步解决方案的高级开发人员。
自定义序列化函数
要执行您自己的自定义序列化,您可以在 NetworkBehaviour 上实现虚拟函数以用于 SyncVar 序列化。这些函数是:
public virtual bool OnSerialize(NetworkWriter writer, bool initialState);
public virtual void OnDeserialize(NetworkReader reader, bool initialState);
使用 initialState 标志来区分第一次序列化游戏对象和何时可以发送增量更新。第一次将游戏对象发送到客户端时,它必须包含完整状态快照,但后续更新可以通过仅包含增量更改来节省带宽。
OnSerialize 函数应返回 true 以指示应发送更新。如果它返回 true,则该脚本的脏位设置为零。如果它返回 false,则不更改脏位。这允许对脚本的多次更改随着时间的推移累积并在系统准备好时发送,而不是每一帧。
OnSerialize 函数仅在 initialState 或 NetworkBehaviour 脏时调用。仅当 SyncVar 或 SyncObject(例如 SyncList)自上次 OnSerialize 调用以来发生更改时,NetworkBehaviour 才会是脏的。发送数据后,NetworkBehaviour 将不会再次变脏,直到下一个 syncInterval(在检查器中设置)。也可以通过手动调用 SetDirtyBit 将 NetworkBehaviour 标记为脏(这不会绕过 syncInterval 限制)。
虽然这可行,但通常最好让 Mirror 生成这些方法并为您的特定字段提供自定义序列化程序。
序列化流程
附加了网络身份组件的游戏对象可以有多个从 NetworkBehaviour 派生的脚本。序列化这些游戏对象的流程是:
在服务端上:
- 每个 NetworkBehaviour 都有一个脏掩码。此掩码在 OnSerialize 中作为 syncVarDirtyBits 使用
- NetworkBehaviour 脚本中的每个 SyncVar 都在脏掩码中分配了一个位。
- 更改 SyncVars 的值会导致在脏掩码中设置该 SyncVar 的位
- 或者,调用 SetDirtyBit 直接写入脏掩码
- 作为update循环的一部分,在服务端上检查 NetworkIdentity 游戏对象
- 如果 NetworkIdentity 上的任何 NetworkBehaviours 是脏的,则为该游戏对象创建一个 UpdateVars 数据包
- 通过在游戏对象上的每个 NetworkBehaviour 上调用 OnSerialize 来填充 UpdateVars 数据包
- 不脏的网络行为在数据包中的脏位写一个零。
- 脏的 NetworkBehaviours 写入其脏掩码,然后写入已更改的 SyncVars 的值
- 如果 OnSerialize 为 NetworkBehaviour 返回 true,则为该 NetworkBehaviour 重置脏掩码,因此在其值更改之前不会再次发送。
- UpdateVars数据包被发送给正在观察游戏对象的准备好的客户端
在客户端:
- 收到游戏对象的 UpdateVars 数据包
- 为游戏对象上的每个 NetworkBehaviour 脚本调用 OnDeserialize 函数
- 游戏对象上的每个 NetworkBehaviour 脚本都会读取一个脏掩码。
- 如果 NetworkBehaviour 的脏掩码为零,则 OnDeserialize 函数返回而不读取任何内容
- 如果脏掩码是非零值,则 OnDeserialize 函数读取与设置的脏位对应的 SyncVars 的值
- 如果有 SyncVar 挂钩函数,则使用从流中读取的值调用这些函数。
所以对于这个脚本:
public class data : NetworkBehaviour
{
[SyncVar(hook = nameof(OnInt1Changed))]
public int int1 = 66;
[SyncVar]
public int int2 = 23487;
[SyncVar]
public string MyString = "Example string";
void OnInt1Changed(int oldValue, int newValue)
{
// do something here
}
}
以下示例显示了 Mirror 为在 NetworkBehaviour.OnSerialize 中调用的 SerializeSyncVars 函数生成的代码:
public override bool SerializeSyncVars(NetworkWriter writer, bool initialState)
{
// 在基类中写入任何 SyncVars
bool written = base.SerializeSyncVars(writer, forceAll);
if (initialState)
{
// 第一次将游戏对象发送到客户端时,发送所有数据(并且没有脏位)
writer.WritePackedUInt32((uint)this.int1);
writer.WritePackedUInt32((uint)this.int2);
writer.Write(this.MyString);
return true;
}
else
{
// 写入已更改的 SyncVars
writer.WritePackedUInt64(base.syncVarDirtyBits);
if ((base.get_syncVarDirtyBits() & 1u) != 0u)
{
writer.WritePackedUInt32((uint)this.int1);
written = true;
}
if ((base.get_syncVarDirtyBits() & 2u) != 0u)
{
writer.WritePackedUInt32((uint)this.int2);
written = true;
}
if ((base.get_syncVarDirtyBits() & 4u) != 0u)
{
writer.Write(this.MyString);
written = true;
}
return written;
}
}
以下示例显示了 Mirror 为在 NetworkBehaviour.OnDeserialize 中调用的 DeserializeSyncVars 函数生成的代码:
public override void DeserializeSyncVars(NetworkReader reader, bool initialState)
{
// 读取基类中的任何 SyncVars
base.DeserializeSyncVars(reader, initialState);
if (initialState)
{
// 第一次将游戏对象发送到客户端时,读取所有数据(并且没有脏位)
int oldInt1 = this.int1;
this.int1 = (int)reader.ReadPackedUInt32();
// 如果新旧值不相等,则调用 hook
if (!base.SyncVarEqual(num, ref this.int1))
{
this.OnInt1Changed(num, this.int1);
}
this.int2 = (int)reader.ReadPackedUInt32();
this.MyString = reader.ReadString();
return;
}
int dirtySyncVars = (int)reader.ReadPackedUInt32();
// 是第一个 SyncVar 脏
if ((dirtySyncVars & 1) != 0)
{
int oldInt1 = this.int1;
this.int1 = (int)reader.ReadPackedUInt32();
//如果新旧值不相等,则调用 hook
if (!base.SyncVarEqual(num, ref this.int1))
{
this.OnInt1Changed(num, this.int1);
}
}
// 第二个 SyncVar 脏了
if ((dirtySyncVars & 2) != 0)
{
this.int2 = (int)reader.ReadPackedUInt32();
}
// 第三个 SyncVar 脏了
if ((dirtySyncVars & 4) != 0)
{
this.MyString = reader.ReadString();
}
}
如果 NetworkBehaviour 的基类也具有序列化函数,则还应调用基类函数。
请注意,为游戏对象状态更新创建的 UpdateVar 数据包在发送到客户端之前可能会在缓冲区中聚合,因此单个传输层数据包可能包含多个游戏对象的更新。