状态同步
状态同步从服务器完成到远程客户端。本地客户端没有序列化到它的数据,因为它与服务器共享场景。序列化到本地客户端的任何数据都是多余的。但SyncVar
回调在本地客户端上调用。
数据不会从远程客户端同步到服务器。这是Commands
的工作。
SyncVars
SyncVars
是从服务器同步到客户端的NetworkBehaviour
脚本的成员变量。当一个对象产生时,或者一个新玩家加入正在进行的游戏时,它们将被发送到它们可见的网络对象上的所有SyncVars
的最新状态。通过使用[SyncVar]
自定义属性将成员变量制作为SyncVars
:
class Player : NetworkBehaviour
{
[SyncVar]
int health;
public void TakeDamage(int amount)
{
if (!isServer)
return;
health -= amount;
}
}
在调用OnStartClient()
之前,将SyncVars
的状态应用于客户端上的对象,因此保证OnStartClient()
内的对象状态为最新状态。
SyncVars
可以是基本类型,例如整数,字符串和浮点数。它们也可以是Unity类型,如Vector3
和用户定义的结构,但是对于结构SyncVars
的更新将作为单片更新发送,而不是结构内的字段更改时的增量更改。一个NetworkBehaviour
脚本最多可以有32个SyncVars
- 这包括SyncLists
。
当SyncVar
的值发生变化时,服务器会自动发送SycnVar
更新。SyncVars
不需要手动弄脏字段。
SyncLists
SyncLists
与SyncVars
类似,但它们是值列表而不是单个值。SyncList
内容包含在使用SyncVar
状态的初始状态更新中。SyncLists
不需要SyncVar
属性,它们是特定的类。有基本类型的内置SyncList类型:
• SyncListString
• SyncListFloat
• SyncListInt
• SyncListUInt
• SyncListBool
还有SyncListStruct
可用于用户定义的结构列表。使用SyncListStruct
派生类的结构可以包含基本类型,数组和常见Unity类型的成员。它们不能包含复杂的类或通用容器。
SyncLists有一个名为Callback的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(SyncListStruct<Buf>.Operation op, int itemIndex)
{
Debug.Log("buf changed:" + op);
}
void Start()
{
m_bufs.Callback = BufChanged;
}
}
自定义序列化函数
通常,SyncVars
的使用足以让脚本将其状态序列化到客户端,但有些情况下需要更复杂的序列化代码。用于SyncVar
序列化的NetworkBehaviour
上的虚拟功能可能会被开发人员用来执行自己的自定义序列化。这些功能是:
public virtual bool OnSerialize(NetworkWriter writer, bool initialState);
public virtual void OnDeSerialize(NetworkReader reader, bool initialState);
initialState
标志可用于区分第一次对象序列化和何时发送增量更新。第一次将对象发送到客户端时,它必须包含完整的状态快照,但后续更新可以通过仅包含增量更改来节省带宽。请注意,当initialState
为true
时,不会调用SyncVar
回调函数,仅用于增量更新。
如果一个类有SyncVars
,那么这些函数的实现会自动添加到类中。所以具有SyncVars
的类不能具有自定义序列化函数。
OnSerialize
函数应返回true
以指示应发送更新。如果它返回true
,那么该脚本的脏位将设置为零,如果它返回false
,则脏位不会更改。这允许对脚本进行多次更改并随时间累积,并在系统准备好时发送,而不是每个帧。
序列化流程
具有NetworkIdentity
组件的游戏对象可以有多个从NetworkBehaviour
派生的脚本。序列化这些对象的流程是:
在服务器上:
• 每个NetworkBehaviour都有一个脏屏蔽。此掩码在OnSerialize中可用作syncVarDirtyBits
• NetworkBehaviour脚本中的每个SyncVar在脏屏蔽中被分配一个位。
• 更改SyncVars的值会导致该SyncVar的位在脏屏蔽中设置
• 或者,调用SetDirtyBit()直接写入脏屏蔽
• NetworkIdentity对象作为其更新循环的一部分在服务器上进行检查
• 如果NetworkIdentity上的任何NetworkBehaviours都是脏的,则会为该对象创建一个UpdateVars数据包
• UpdateVars数据包通过在对象上的每个NetworkBehaviour上调用OnSerialize来填充
• 没有脏的NetworkBehaviours会为数据包的脏位写零
• 肮脏的NetworkBehaviours会写入其肮脏的掩码,然后是已更改的SyncVars的值
• 如果对于NetworkBehaviour而言,OnSerialize返回true,则将为该NetworkBehaviour重置脏掩码,因此它将不会再次发送,直到其值发生更改。
• UpdateVars数据包发送到正在观察该对象的就绪客户端
在客户端:
• 接收到一个对象的UpdateVars数据包
• OnDeserialize函数为对象上的每个NetworkBehaviour脚本调用
• 对象上的每个NetworkBehaviour脚本都会读取一个脏屏蔽。
• 如果NetworkBehaviour的脏屏蔽为零,则OnDeserialize函数将不会再读取而返回
• 如果脏掩码为非零值,则OnDeserialize函数将读取与设置的脏位相对应的SyncVars的值
• 如果存在SyncVar回调函数,则使用从流中读取的值调用这些函数。
所以对于这个脚本:
public class data : NetworkBehaviour
{
[SyncVar]
public int int1 = 66;
[SyncVar]
public int int2 = 23487;
[SyncVar]
public string MyString = "esfdsagsdfgsdgdsfg";
}
生成的OnSerialize函数如下所示:
public override bool OnSerialize(NetworkWriter writer, bool forceAll)
{
if (forceAll)
{
// the first time an object is sent to a client, send all the data (and no dirty bits)
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;
}
OnDeserialize函数是这样的:
public override void OnDeserialize(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
也具有序列化函数的基类,则还应该调用基类函数。
请注意,为对象状态更新创建的UpdateVar
数据包在发送到客户端之前可能会在缓冲区中聚合,因此单个传输层数据包可能包含多个对象的更新。
🔚