unet syncvar_UNET SyncVar

unet syncvar

这篇文章讨论了网络Unity游戏中的状态同步。 我们首先总结一下在现有的(传统)Unity网络系统中如何完成此工作,然后继续介绍它如何在新的UNET网络系统中发挥作用以及进行该新设计的决策过程。 (This post discusses state synchronization in networked Unity games. We begin with a summary of how this is done in the existing (legacy) Unity networking system, and move on to how it will function in the new UNET networking system and the decision making process that lead to that new design.)

背景和要求 (Background and Requirements)

As a little bit of background. It is common for networked games to have a server that owns objects, and clients that need to be told when data in those objects change. For example in a combat game, the health of the players needs to visible to all players. This requires a member variable on a script class that is sent to all of the clients when it changes on the server. Below is a simple combat class:

作为一点背景。 联网游戏通常拥有拥有对象的服务器,并且需要告知客户端这些对象中的数据何时发生更改。 例如,在格斗游戏中,玩家的健康需要对所有玩家可见。 这需要脚本类上的成员变量,该变量在服务器上更改时将发送给所有客户端。 下面是一个简单的战斗课:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Combat : MonoBehaviour
{
    public int Health;
    public bool Alive;
    public void TakeDamage(int amount)
    {
        if (amount >= Health) {
            Alive = false;
            Health = 0;
        } else {
            Health -= amount;
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Combat : MonoBehaviour
{
     public int Health ;
     public bool Alive ;
     public void TakeDamage ( int amount )
     {
         if ( amount & gt ; = Health ) {
             Alive = false ;
             Health = 0 ;
         } else {
             Health -= amount ;
         }
     }
}

When a player on the server takes damage, all of the players in the game need to be told about the new health value for that player.

当服务器上的玩家受到伤害时,需要告知游戏中的所有玩家该玩家的新生命值。

This seems simple, but the challenge here is to make the system invisible to developers writing the code, efficient in terms of CPU, memory and bandwidth usage, and flexible enough to support all the types the developer wants to use. So some concrete goals for this system would be:

这看起来很简单,但是这里的挑战是使开发人员无法编写系统来编写代码,从而在CPU,内存和带宽使用方面高效,并且足够灵活以支持开发人员要使用的所有类型。 因此,此系统的一些具体目标是:

1.      Minimize memory usage by not keeping shadow copies of variables

1.通过不保留变量的影子副本来最大程度地减少内存使用

2.      Minimize bandwidth usage by only sending states that have changed (incremental updates)

2.通过仅发送已更改的状态(增量更新)来最小化带宽使用

3.      Minimize CPU usage by not constantly checking to see if a state has changed

3.通过不经常检查状态是否已更改来最小化CPU使用率

4.      Minimize protocol and serialization mismatch issues, by not requiring developers to hand-code serialization functions

4.通过不要求开发人员手动编码序列化功能,将协议和序列化不匹配问题最小化

5.      Don’t require developers to explicitly set variables as dirty

5.不需要开发人员明确将变量设置为脏变量

6.      Work with all supported Unity scripting languages

6.使用所有受支持的Unity脚本语言

7.      Don’t disrupt developer workflow

7.不要破坏开发人员的工作流程

8.      Don’t introduce manual steps that developers need to perform to use the system

8.不要介绍开发人员使用系统需要执行的手动步骤

9.      Allow the system to be driven by meta-data (custom attributes)

9.允许系统由元数据(自定义属性)驱动

10.   Handle both simple and complex types

10.处理简单类型和复杂类型

11.   Avoid reflection at runtime

11.避免在运行时反射

This is an ambitious list of requirements!

这是一个雄心勃勃的要求清单!

传统网络系统 (Legacy Networking System)

The existing Unity networking system has a “ReliableDeltaCompressed” type of synchronization that performs state synchronization by providing an OnSerializeNetworkView() hook function. This function is invoked on objects with the NetworkView component, and the serialization code written by the developer writes to (or reads from) the byte stream provided. The contents of this byte stream are then cached by the engine, and if the next time the function is called the result doesn’t match the cached version the object is considered dirty and the state is sent to clients. To take an example, a serialization function could look like this:

现有的Unity网络系统具有“ ReliableDeltaCompressed”类型的同步,该同步通过提供OnSerializeNetworkView()挂钩函数来执行状态同步。 使用NetworkView组件在对象上调用此函数,并且开发人员编写的序列化代码写入(或从中读取)提供的字节流。 然后,该字节流的内容由引擎缓存,如果下次调用该函数的结果与缓存的版本不匹配,则该对象将被视为脏对象,并将状态发送给客户端。 举个例子,序列化函数可能看起来像这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void OnSerializeNetworkView (Bitstream stream, NetworkMessageInfo info)
{
    float horizontalInput = 0.0f;
    if (stream.isWriting) {
        // Sending
        horizontalInput = Input.GetAxis ("Horizontal");
       stream.Serialize (horizontalInput);
    } else {
        // Receiving
        stream.Serialize (horizontalInput);
        // ... do something meaningful with the received variable
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
void OnSerializeNetworkView ( Bitstream stream , NetworkMessageInfo info )
{
     float horizontalInput = 0.0f ;
     if ( stream . isWriting ) {
         // Sending
         horizontalInput = Input . GetAxis ( "Horizontal" ) ;
       stream . Serialize ( horizontalInput ) ;
     } else {
         // Receiving
         stream . Serialize ( horizontalInput ) ;
         // ... do something meaningful with the received variable
     }
}

This approach meets some of the requirements listed above, but not all of them. It is automatic at runtime, since OnSerializeNetworkView() is invoked by the engine at the network send rate, and the developer doesn’t need to set variables as dirty. It also doesn’t add any extra steps to the build process or disrupt the developer workflow.

此方法满足上面列出的一些要求,但不是全部。 由于引擎以网络发送速率调用OnSerializeNetworkView(),因此它在运行时是自动的,并且开发人员无需将变量设置为脏。 它还不会在构建过程中添加任何额外的步骤,也不会破坏开发人员的工作流程。

But, its performance is not great – especially when there are many networked objects. CPU time is spent on comparisons, and memory is used for caching copies of byte streams.  It is also susceptible to mismatch errors in the serialization function because it has to be updated by hand when new member variables are added that need to be synchronized. It is also not driven by metadata, so the editor and other tools cannot be aware of what variables are synchronized.

但是,它的性能不是很好-尤其是当有许多联网对象时。 CPU时间用于比较,并且内存用于缓存字节流的副本。 它也容易受到序列化函数中不匹配错误的影响,因为当添加需要同步的新成员变量时,必须手动更新它。 它也不受元数据驱动,因此编辑器和其他工具无法知道要同步哪些变量。

SyncVars的代码生成 (Code Generation for SyncVars)

As the UNET team worked on the new state synchronization system, the solution we came up with was a code generator driven by custom attributes. In user code, this looks like:

当UNET团队研究新的状态同步系统时,我们想到的解决方案是由自定义属性驱动的代码生成器。 在用户代码中,这看起来像:

1
2
3
4
5
6
7
8
9
using UnityEngine.UNetwork;
class Combat : UNetBehaviour
{
    [SyncVar]
    public int Health;
    [SyncVar]
    public bool Alive;
}
1
2
3
4
5
6
7
8
9
using UnityEngine . UNetwork ;
class Combat : UNetBehaviour
{
     [ SyncVar ]
     public int Health ;
     [ SyncVar ]
     public bool Alive ;
}

This new custom attribute tells the system that the Health  and Alive member variables need to be synchronized. Now, the developer doesn’t need to write a serialization function, since the code generator has the custom attribute data and it can generate perfect serialization and unserialization functions with the right ordering and types. This generated function looks something like this:

这个新的自定义属性告诉系统Health和Alive成员变量需要同步。 现在,开发人员无需编写序列化函数,因为代码生成器具有自定义属性数据,并且可以使用正确的顺序和类型生成完美的序列化和反序列化函数。 这个生成的函数看起来像这样:

1
2
3
4
5
public override void UNetSerializeVars(UWriter writer)
{
    writer.WriteInt(Health);
    writer.WriteBool(Alive);
}
1
2
3
4
5
public override void UNetSerializeVars ( UWriter writer )
{
     writer . WriteInt ( Health ) ;
     writer . WriteBool ( Alive ) ;
}

Since this overrides a virtual function on the UNetBehaviour base class, when the game object is serialized, the script variables will also be automatically serialized. Then, they will be unpacked at the other end with a matching code-generated unserialization function. So there is no chance of mismatches, and the code updates automatically when a new [SyncVar] variable is added.

因为这将覆盖UNetBehaviour基类上的虚函数,所以当游戏对象被序列化时,脚本变量也将被自动序列化。 然后,将使用匹配的代码生成的反序列化函数在另一端将它们解压缩。 因此,不会出现不匹配的情况,并且在添加新的[SyncVar]变量后代码会自动更新。

This data is now available to the editor, so the inspector window can show more detail like this:

该数据现在可供编辑器使用,因此检查器窗口可以显示更多详细信息,如下所示:

unet syncvar_UNET SyncVar

But there are still some issues here. This function sends all the state all the time – it is not incremental; so if a single variable on an object changes, the entire object state would be sent. Also, how do we know when this serialization function should be called? It is not efficient to send states when nothing has changed.

但是这里仍然有一些问题。 此功能始终发送所有状态-它不是增量状态; 因此,如果对象上的单个变量发生更改,则将发送整个对象状态。 另外,我们如何知道何时应调用此序列化函数? 没有任何变化时发送状态效率不高。

We wrestled with using properties and dirty flags to do this. It seemed natural that a property could wrap each [SyncVar] variable and set dirty flags when something changes. This approach was partially successful. Having a bitmask of dirty flags lets the code generator make code to do incremental updates. That generated code would look something like this:

我们努力使用属性和脏标志来做到这一点。 一个属性似乎可以包装每个[SyncVar]变量并在发生更改时设置脏标志,这似乎很自然。 这种方法部分成功。 带有脏标志的位掩码使代码生成器可以使代码进行增量更新。 生成的代码如下所示:

1
2
3
4
5
6
7
public override void UNetSerializeVars(UWriter writer)
{
    Writer.Write(m_DirtyFlags)
    if (m_DirtyFlags & 0x01) { writer.WriteInt(Health); }
    if (m_DirtyFlags & 0x02) { writer.WriteBool(Alive); }
    m_DirtyFlags = 0;
}
1
2
3
4
5
6
7
public override void UNetSerializeVars ( UWriter writer )
{
     Writer . Write ( m_DirtyFlags )
     if ( m_DirtyFlags & amp ; 0x01 ) { writer . WriteInt ( Health ) ; }
     if ( m_DirtyFlags & amp ; 0x02 ) { writer . WriteBool ( Alive ) ; }
     m_DirtyFlags = 0 ;
}

In this way, the unserialization function can read the dirty flags mask and only unserialize variables written to the stream. This makes for efficient bandwidth usage, and lets us know when the object is dirty. Plus, it’s still all automatic for the user. But how do these properties work?

通过这种方式,反序列化功能可以读取脏标志掩码,并且仅反序列化写入流的变量。 这样可以有效利用带宽,并让我们知道对象何时变脏。 另外,对于用户而言,这仍然是全自动的。 但是这些属性如何工作?

Say we try wrapping the [SyncVar] member variable:

假设我们尝试包装[SyncVar]成员变量:

1
2
3
4
5
6
7
8
9
10
11
12
using UnityEngine.UNetwork;
class Combat : UNetBehaviour
{
    [SyncVar]
    public int Health;
    // generated property
    public int HealthSync {
        get { return Health; }
        set { m_dirtyFlags |= 0x01;  Health = value; }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
using UnityEngine . UNetwork ;
class Combat : UNetBehaviour
{
     [ SyncVar ]
     public int Health ;
     // generated property
     public int HealthSync {
         get { return Health ; }
         set { m_dirtyFlags |= 0x01 ;    Health = value ; }
     }
}

This does the job but it has the wrong name. The TakeDamage() function from above uses Health not HealthSync, so it bypasses the property.  The user can’t even use the HealthSync property directly since it doesn’t even exist until code generation happens.  It could be made into a two-phase process where the code generation step happens, then the user updates their code – but this is fragile. It is prone to compilation errors that can’t be fixed without undoing large chunks of code.

这可以完成工作,但是名称错误。 上面的TakeDamage()函数使用Health而不是HealthSync,因此它绕过该属性。 用户甚至无法直接使用HealthSync属性,因为在代码生成之前,该属性甚至不存在。 可以将其分为两个阶段,在此过程中执行代码生成步骤,然后用户更新其代码-但这很脆弱。 如果不撤消大块代码,就很容易解决编译错误。

Another approach would be to require developers to write the above property code for each [SyncVar] variable.  But that is work for developers, and potentially error prone.  The bitmasks in user-written and generated code would have to match up exactly for this to work, so adding and removing [SyncVar] variables would be delicate.

另一种方法是要求开发人员为每个[SyncVar]变量编写以上属性代码。 但这对开发人员是可行的,并且可能容易出错。 用户编写的代码和生成的代码中的位掩码必须完全匹配才能起作用,因此添加和删除[SyncVar]变量将很麻烦。

输入Mono Cecil (Enter Mono Cecil)

So we need to be able to generate wrapper properties and make existing code use them even if that code isn’t even aware of their existence. Well, fortunately there is a tool for Mono called Cecil which does exactly this. Cecil is able to load Mono assemblies in the ECMA CIL format, modify them and write them back out.

因此,即使该代码甚至不知道它们的存在,我们也需要能够生成包装器属性并使现有代码使用它们。 好吧,幸运的是,Mono有一个名为Cecil的工具可以做到这一点。 Cecil能够以ECMA CIL格式加载Mono程序集,对其进行修改并写回。

This is where is gets a little crazy. The UNET code generator creates the wrapper properties, then it finds all of the code sites where the original member variables were accessed. It then replaces the references to the member variables with references to the wrapper properties and Voila! Now the user code is calling through the newly created properties without any work from the user.

这是有点疯狂的地方。 UNET代码生成器创建包装器属性,然后查找访问原始成员变量的所有代码站点。 然后,它将对成员变量的引用替换为对包装器属性和Voila!的引用。 现在,用户代码正在通过新创建的属性进行调用,而无需用户进行任何工作。

Since Cecil operates at the CIL level, it has the added advantage of working with all languages since they all compile down to the same instruction format.

由于Cecil在CIL级别上运行,因此它具有与所有语言一起使用的附加优势,因为它们都可以编译为相同的指令格式。

The generated CIL for a final serialization function that gets injected into the script assembly now looks like this:

现在为最终序列化函数生成的CIL被注入到脚本程序集中,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
IL_0000: ldarg.2
IL_0001: brfalse IL_000d
IL_0006: ldarg.0
IL_0007: ldc.i4.m1
IL_0008: stfld uint32 [UnityEngine]UnityEngine.UNetBehaviour::m_DirtyBits
IL_000d: nop
IL_000e: ldarg.1
IL_000f: ldarg.0
IL_0010: ldfld uint32 [UnityEngine]UnityEngine.UNetBehaviour::m_DirtyBits
IL_0015: callvirt instance void [UnityEngine]UnityEngine.UNetwork.UWriter::UWriteUInt32(uint32)
IL_001a: ldarg.0
IL_001b: ldfld uint32 [UnityEngine]UnityEngine.UNetBehaviour::m_DirtyBits
IL_0020: ldc.i4 1
IL_0025: and
IL_0026: brfalse IL_0037
IL_002b: ldarg.1
IL_002c: ldarg.0
IL_002d: ldfld valuetype Buf/BufType Powerup::mbuf
IL_0032: callvirt instance void [mscorlib]System.IO.BinaryWriter::Write(int32)
IL_0037: nop
IL_0038: ldarg.0
IL_0039: ldc.i4.0
IL_003a: stfld uint32 [UnityEngine]UnityEngine.UNetBehaviour::m_DirtyBits
IL_003f: ret
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
IL_0000 : ldarg . 2
IL_0001 : brfalse IL_000d
IL_0006 : ldarg . 0
IL_0007 : ldc . i4 . m1
IL_0008 : stfld uint32 [ UnityEngine ] UnityEngine . UNetBehaviour :: m_DirtyBits
IL_000d : nop
IL_000e : ldarg . 1
IL_000f : ldarg . 0
IL_0010 : ldfld uint32 [ UnityEngine ] UnityEngine . UNetBehaviour :: m_DirtyBits
IL_0015 : callvirt instance void [ UnityEngine ] UnityEngine . UNetwork . UWriter :: UWriteUInt32 ( uint32 )
IL_001a : ldarg . 0
IL_001b : ldfld uint32 [ UnityEngine ] UnityEngine . UNetBehaviour :: m_DirtyBits
IL_0020 : ldc . i4 1
IL_0025 : and
IL_0026 : brfalse IL_0037
IL_002b : ldarg . 1
IL_002c : ldarg . 0
IL_002d : ldfld valuetype Buf / BufType Powerup :: mbuf
IL_0032 : callvirt instance void [ mscorlib ] System . IO . BinaryWriter :: Write ( int32 )
IL_0037 : nop
IL_0038 : ldarg . 0
IL_0039 : ldc . i4 . 0
IL_003a : stfld uint32 [ UnityEngine ] UnityEngine . UNetBehaviour :: m_DirtyBits
IL_003f : ret

Luckily ILSpy can convert between CIL and C# in both directions, so in this case it allows us view the generated CIL code as C#. ILSpy is a great tool for working with Mono/.Net assemblies. The C# looks like:

幸运的是,ILSpy可以在两个方向上在CIL和C#之间进行转换,因此在这种情况下,它使我们可以将生成的CIL代码视为C#。 ILSpy是用于处理Mono / .Net程序集的出色工具。 C#看起来像:

1
2
3
4
5
6
7
8
9
10
11
12
13
<span style="color: #0000ff;">public</span> <span style="color: #993300;">override</span> <span style="color: #ff0000;">void</span> <strong><span style="color: #000080;">UNetSerializeVars</span></strong>(UWriter writer, <span style="color: #ff0000;">bool</span> forceAll)
{
    <span style="color: #0000ff;">if</span> (forceAll)
    {
        <strong>this</strong>.m_DirtyBits = <span style="color: #000080;">4294967295</span>u;
    }
    writer.<strong><span style="color: #000080;">UWriteUInt32</span></strong>(<strong>this</strong>.m_DirtyBits);
    <span style="color: #0000ff;">if</span> ((<strong>this</strong>.m_DirtyBits &amp; 1u) != 0u)
    {
        writer.<strong><span style="color: #000080;">Write</span></strong>((<strong><span style="color: #ff0000;">int</span></strong>)<strong>this</strong>.mbuf);
    }
    <strong>this</strong>.m_DirtyBits = 0u;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
< span style = "color: #0000ff;" > public < / span > < span style = "color: #993300;" > override < / span > < span style = "color: #ff0000;" > void < / span > < strong > < span style = "color: #000080;" > UNetSerializeVars < / span > < / strong > ( UWriter writer , < span style = "color: #ff0000;" > bool < / span > forceAll )
{
     < span style = "color: #0000ff;" > if < / span > ( forceAll )
     {
         < strong > this < / strong > . m_DirtyBits = < span style = "color: #000080;" > 4294967295 < / span > u ;
     }
     writer . < strong > < span style = "color: #000080;" > UWriteUInt32 < / span > < / strong > ( < strong > this < / strong > . m_DirtyBits ) ;
     < span style = "color: #0000ff;" > if < / span > ( ( < strong > this < / strong > . m_DirtyBits & amp ; 1u ) != 0u )
     {
         writer . < strong > < span style = "color: #000080;" > Write < / span > < / strong > ( ( < strong > < span style = "color: #ff0000;" > int < / span > < / strong > ) < strong > this < / strong > . mbuf ) ;
     }
     < strong > this < / strong > . m_DirtyBits = 0u ;
}

So let’s see how this meets our requirements:

因此,让我们看看这如何满足我们的要求:

1.      No shadow copies of variables

1.没有变量的影子副本

2.      Incremental updates

2.增量更新

3.      No comparison checks for state changes

3.没有比较检查状态变化

4.      No hand-coded serialization functions

4.没有手动编码的序列化功能

5.      No explicit dirty calls

5.没有明确的脏电话

6.      Works with all supported Unity scripting languages

6.与所有受支持的Unity脚本语言一起使用

7.      No workflow changes for the developer

7.开发人员无需更改工作流程

8.      No manual steps for the developer to perform

8.开发人员无需执行任何手动步骤

9.      Driven by meta-data

9.由元数据驱动

10.   Handles all types (with new UWriter/UReader serializers)

10.处理所有类型(使用新的UWriter / UReader序列化器)

11.   No reflection at runtime

11.运行时无反射

Looks like we have them all covered.  This system will be efficient and friendly to developers. Hopefully it will help make developing multiplayer games with Unity easier for everyone.

看起来我们都覆盖了它们。 该系统将对开发人员高效且友好。 希望这将使每个人都能更轻松地使用Unity开发多人游戏。

We also use Cecil for RPC call implementations to avoid looking up functions by name with reflection. More on that in a later blog post.

我们还将Cecil用于RPC调用实现,以避免使用反射名按名称查找函数。 在后面的博客文章中可以找到更多信息。

翻译自: https://blogs.unity3d.com/2014/05/29/unet-syncvar/

unet syncvar

相关文章:

  • 2021-09-05
  • 2021-11-07
  • 2021-09-12
  • 2021-11-14
  • 2021-09-25
  • 2021-10-11
  • 2021-11-05
  • 2021-07-17
猜你喜欢
  • 2021-06-16
  • 2022-02-24
  • 2021-12-16
  • 2021-04-26
  • 2021-07-22
  • 2021-08-22
  • 2022-01-11
相关资源
相似解决方案