在网络游戏中,大部分事务都是在服务器内进行和处理的,例如NPC被玩家攻击后减HP,服务器需要将变化的HP值传到各个客户端,客户端更新此HP值后,玩家们电脑中的NPC才会掉血,这种同步变量是网络游戏中最基本也是最频繁的操作。
那么如何实现这种变量同步?最暴力的方法可以是服务器每帧都将此HP发给客户端。例如:
void Update () {
byte[] hp_Byte=BitConverter.GetBytes(hp);//1,将变量序列化为二进制格式
myServer.SendToAll(hp_Byte,msgType_hpNPC1);//2,伪代码。将变量传给所有客户端,并通知它们此消息为NPC1的hp值。
}
但就算是一个小游戏,需要同步的变量也会很多,这种方法显然在性能上是无法接受的。合理的是只同步发生变化的变量,这里要用到脏标识,dirty bit。
int m_DirtyBit=0;
00000000 00000000 00000000 00000000 //每一位0记录一个变量是否在此帧内被更改过。
通过与2的0~31次方进行位运算单独的更改胀标的每一位。
例如我们可以这样实现:
private long dirty_bit=0;
private int _npc1_hp=100;
public int npc1_hp{
get{
return _npc1_hp;
}
set{
dirty_bit|=0x01;//一旦此变量发生变化,则将dirty_bit的某位变为1
_npc1_hp=value;
}
}
private int _npc2_hp=100;
public int npc2_hp{
get{
return _npc2_hp;
}
set{
dirty_bit|=0x02;//以此类推
_npc2_hp=value;
}
}
void Update () {
SyncVars();
}
private void SyncVars(){//然后在同步变量方法内通过检查脏位的各个位来判断哪些变量发生了变化并发送给各个客户端。
if(dirty_bit&0x01){//如果脏位第一个bit为1则为true
myServer.SendToAll(BitConverter.GetBytes(_npc1_hp,msgType.npc1HP));
}
if(dirty_bit&0x02){
myServer.SendToAll(BitConverter.GetBytes(_npc2_hp,msgType.npc2HP));
}
dirty_bit=0;
}
那么在Unity中,Networking模块通过一个[Syncvar]特性就完成了上面的所有内容,如果在hp变量上方加上一个[Syncvar]特性,那么一旦此变量发生变化,服务器就会将此变量发送给所有客户端。开发者不再需要担心序列化,脏标识判断以及send方法
[SyncVar]
private int _npc1_hp=100;
通过官方文档介绍,每个NetworkBehaviour类最多只可以有32个标为SyncVar特性的变量,这说明了每个NetworkBehaviour都只有一个32位int的胀标。
[EditorBrowsable(EditorBrowsableState.Never)]
protected void SetSyncVar<T>(T value, ref T fieldValue, uint dirtyBit)
{
bool changed = false;
if (value == null)
{
if (fieldValue != null)
changed = true;
}
else
{
changed = !value.Equals(fieldValue);
}
if (changed)
{
if (LogFilter.logDev) { Debug.Log("SetSyncVar " + GetType().Name + " bit [" + dirtyBit + "] " + fieldValue + "->" + value); }
SetDirtyBit(dirtyBit);
fieldValue = value;
}
}
NetworkBehaviour源码中有这么一段,changed = !value.Equals(fieldValue); , 说明只有赋予SyncVar的值与前一个值不同时变量才会变脏。
SyncVar还支持一个hook特性,当SyncVar值在客户端发生变化时进入一个方法,方便开发者插入逻辑。例如下面的方法可让服务器和所有的客户端UI更新NPC1的血条。
[SyncVar(hook="On_npc1_hp"]
private int _npc1_hp=100;
void On_npc1_hp(int newValue){
//变量被访问之后会执行此方法,newValue为变量被赋的值,类型要与SyncVar的变量类型一致。
}
或
[SyncVar(hook="OnScale"]
public int scale_SV=1;
void OnScale(int newScale){
scale_SV=newScale;
transform.localScale=newScale;
}
当服务器上的此游戏物体大小发生变化和后,通过这种方式使客户端的此物体也发生变化。
经测试发现,客户端方面hook函数调用完成后相关SyncVar才会完成同步。
SyncVar的实现用到了一个叫Mono Cecil的技术。简单来说,此技术允许编译器在编译阶段插入其他的CIL中间代码。Unity应该是在生成CIL中间代码的阶段,寻找所有带有SyncVar特性的变量,并插入上文类似的属性设置器或一个包装方法(方法内改变变量并进行脏位运算),然后将项目中所有引用此变量的引用转到带有脏位操作的方法或属性设置器处。
参考:
https://blogs.unity3d.com/cn/2014/05/29/unet-syncvar/
————————————————
-2017-5-5更新
-2017-5-8更新
-2017-5-10更新
-2017-5-12更改