• MLAPI系列 - 04 - 网络变量和网络序列化【网络同步】


    一、网络同步概述

    Netcode的网络同步手段主要有两种:第一是RPC机制,远程调用,第二是使用网络变量。

    二、 网络变量

    网络变量属于Netcode的特有网络类型,封装维护一个Value,如果要封装多个字段或者数组需要自行进行封装。

    三、 关于框架内置序列化

    • 1 网络变量被定义为泛型类 NetworkVariable,支持C#基本类型、Unity基本类型、自定义枚举。

    • 2 RPC传递消息参数,需要使用可序列化类型,支持以下可序列化类型以及继承序列化接口INetworkSerializable的自定义序列化类型。

    1 C#基础类型

    C#基础类型将由内置的序列化代码进行序列化。
    这些类型包括:bool, char, sbyte, byte, short, ushort, int, uint, long, ulong, float, double, and string.

    [ServerRpc]
    void FooServerRpc(int somenumber, string sometext) { /* ... */ }
    
    void Update()
    {
        if (Input.GetKeyDown(KeyCode.P))
        {
            // Client -> Server
            FooServerRpc(Time.frameCount, "hello, world"); 
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    2 Unity基础类型

    Unity基础类型 Color, Color32, Vector2, Vector3, Vector4, Quaternion, Ray, Ray2D类型将由内置序列化代码序列化。

    [ClientRpc]
    void BarClientRpc(Color somecolor) { /* ... */ }
    
    void Update()
    {
        if (Input.GetKeyDown(KeyCode.P))
        {
            BarClientRpc(Color.red); // Server -> Client
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    3 枚举类型

    用户定义的枚举类型将由内置序列化代码序列化(使用基础整数类型)

    enum SmallEnum : byte // 0-255 限定长度
    {
        A,
        B,
        C
    }
    
    enum NormalEnum // default -> int
    {
        X,
        Y,
        Z
    }
    
    
    [ServerRpc]
    void ConfigServerRpc(SmallEnum smallEnum, NormalEnum normalEnum) 
    { /* ... */ }
    
    
    void Update()
    {
        if (Input.GetKeyDown(KeyCode.P))
        {
            ConfigServerRpc(SmallEnum.A, NormalEnum.X); // Client -> Server
        }
    }
    
    • 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

    4 数组

    像 int[] 这样的数组 由内置的序列化代码序列化:

    如果它们的基础类型是支持序列化的类型之一(如Vector3) 或者 它们是否实现了INetworkSerializable接口。

    [ServerRpc]
    void HelloServerRpc(int[] scores, Color[] colors) { /* ... */ }
    
    [ClientRpc]
    void WorldClientRpc(MyComplexType[] values) { /* ... */ }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    5 INetworkSerializable接口

    INetworkSerializable 接口可用于定义自定义的可序列化类型。

    struct MyComplexStruct : INetworkSerializable
    {
        public Vector3 Position;
        public Quaternion Rotation;
    
        // INetworkSerializable
        void NetworkSerialize<T>(BufferSerializer<T> serializer) where T : IReaderWriter
        {
            serializer.SerializeValue(ref Position);
            serializer.SerializeValue(ref Rotation);
        }
        // ~INetworkSerializable
    }
    
    //实现类型INetworkSerializable,并被NetworkSerializer, RPCs和NetworkVariable支持
    
    [ServerRpc]
    void MyServerRpc(MyComplexStruct myStruct) { /* ... */ }
    
    void Update()
    {
        if (Input.GetKeyDown(KeyCode.P))
        {
            MyServerRpc(
                new MyComplexStruct
                {
                    Position = transform.position,
                    Rotation = transform.rotation
                }); // Client -> Server
        }
    }
    
    • 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
    • 29
    • 30
    • 31

    5.1 嵌套序列类型

    嵌套序列类型将被null除非您按照以下方法之一初始化:

    • 调用前手动SerializeValue如果serializer.IsReader(或者类似的东西)
    • 在默认构造函数中初始化

    这是故意的。在正确初始化之前,您可能会看到这些值为null。序列化程序没有反序列化它们,因此null值在序列化之前被简单地应用。

    5.2 条件序列化

    因为对结构的序列化有更多的控制,所以可以在运行时实现条件序列化。

    示例:数组

    public struct MyCustomStruct : INetworkSerializable
    {
        public int[] Array;
    
        void NetworkSerialize<T>(BufferSerializer<T> serializer) where T : IReaderWriter
        {
            // Length
            int length = 0;
            if (!serializer.IsReader)
            {
                length = Array.Length;
            }
    
            serializer.SerializeValue(ref length);
    
            // Array
            if (serializer.IsReader)
            {
                Array = new int[length];
            }
    
            for (int n = 0; n < length; ++n)
            {
                serializer.SerializeValue(ref Array[n]);
            }
        }
    }
    
    • 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

    读取:

    序列化(反序列化)length从流回来
    迭代Array成员n=length倍 将值序列化(反序列化)回数组[n]流中的元素

    写入:

    序列化长度=数组 写入流的长度
    迭代数组成员n =长度倍 序列化数组中的值[n]元素添加到流中
    这BufferSerializer.IsReader标志在这里被用来确定是否设置length值,然后我们使用它来确定是否创建一个新的int[]实例与length要设置的大小Array在从流中读取值之前。
    还有一个等价但相反的BufferSerializer.IsWriting

    示例:移动

    public struct MyMoveStruct : INetworkSerializable
    {
        public Vector3 Position;
        public Quaternion Rotation;
    
        public bool SyncVelocity;
        public Vector3 LinearVelocity;
        public Vector3 AngularVelocity;
    
        void NetworkSerialize<T>(BufferSerializer<T> serializer) where T : IReaderWriter
        {
            // Position & Rotation
            serializer.SerializeValue(ref Position);
            serializer.SerializeValue(ref Rotation);
            
            // LinearVelocity & AngularVelocity
            serializer.SerializeValue(ref SyncVelocity);
            if (SyncVelocity)
            {
                serializer.SerializeValue(ref LinearVelocity);
                serializer.SerializeValue(ref AngularVelocity);
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    读取:

    序列化(反序列化)Position从流回来
    序列化(反序列化)Rotation从流回来
    序列化(反序列化)SyncVelocity从流回来
    检查是否SyncVelocity设置为true,如果是这样:
    序列化(反序列化)LinearVelocity从流回来
    序列化(反序列化)AngularVelocity从流回来

    写入:

    加载Position到流里
    加载Rotation到流里
    加载SyncVelocity到流里
    检查是否SyncVelocity设置为true,如果是这样:
    加载LinearVelocity到流里
    加载AngularVelocity到流里
    不同于排列上例中,我们没有使用BufferSerializer.IsReader标志来更改序列化逻辑,但要更改序列化标志本身的值。

    如果SyncVelocity标志设置为真,则LinearVelocity和AngularVelocity将被序列化到流中
    当SyncVelocity标志设置为false,我们会离开LinearVelocity和AngularVelocity使用默认值。

    5.3 递归嵌套序列化

    可以用递归序列化嵌套成员INetworkSerializable层次树中的接口。

    示例:

    public struct MyStructA : INetworkSerializable
    {
        public Vector3 Position;
        public Quaternion Rotation;
    
        void NetworkSerialize<T>(BufferSerializer<T> serializer) where T : IReaderWriter
        {
            serializer.SerializeValue(ref Position);
            serializer.SerializeValue(ref Rotation);
        }
    }
    
    public struct MyStructB : INetworkSerializable
    {
        public int SomeNumber;
        public string SomeText;
        public MyStructA StructA;
        
        void NetworkSerialize<T>(BufferSerializer<T> serializer) where T : IReaderWriter
        {
            serializer.SerializeValue(ref SomeNumber);
            serializer.SerializeValue(ref SomeText);
            StructA.NetworkSerialize(serializer);
        }
    }
    
    • 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

    如果我们要单独加载MyStructA,它会使用NetworkSerializer加载Position和Rotation入流,.

    然而,如果我们要序列化MyStructB,它会序列化SomeNumber和SomeText到流中,然后序列化StructA通过Call -
    MyStructA void
    NetworkSerialize(NetworkSerializer)方法,该方法序列化Position和Rotation入同一流。

    注意
    从技术上讲,数量没有硬性限制INetworkSerializable可以沿着树层次结构序列化的字段。在实践中,考虑内存和带宽边界以获得最佳性能。

    您可以在递归嵌套序列化场景中有条件地序列化,并利用这两种功能。

    6 自定义序列化

    使用时RPCs,NetworkVariable或任何其他需要序列化的游戏对象网络代码(Netcode)相关任务。Netcode使用默认的序列化管道,如下所示:

    Custom Types => Built In Types => INetworkSerializable
    
    • 1

    也就是说,当Netcode第一次获得一个类型时,它将检查用户已经为序列化注册的任何自定义类型,之后它将检查它是否是一个内置类型,如Vector3、float等。这些是默认处理的。如果不是,它将检查该类型是否继承INetworkSerializable如果有,它将调用它的write方法。

    默认情况下,任何满足unmanaged通用约束可以自动序列化为RPC参数。这包括所有基本类型(bool、byte、int、float、enum等)以及只包含这些基本类型的任何结构。

    通过这个流程,您可以覆盖全部的序列化全部类型,甚至内置在类型中,并且使用提供的API,它甚至可以用您自己没有定义的类型来完成,那些在第三方墙后面的类型,例如。网络类型。

    若要注册自定义类型或重写已处理的类型,需要为FastBufferReader.ReadValueSafe()和FastBufferWriter.WriteValueSafe():

    //告诉Netcode将来如何序列化和反序列化Url。
    //类名在这里无关紧要。
    public static class SerializationExtensions
    {
        public static void ReadValueSafe(this FastBufferReader reader, out Url value)
        {
            reader.ReadValueSafe(out string val);
            value = new Url(val);
        }
    
        public static void WriteValueSafe(this FastBufferWriter writer, in Url value)
        {
            writer.WriteValueSafe(instance.Value);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    RPC的代码生成将直接通过FastBufferWriterFastBufferReader 自动获取并使用这些函数。

    您还可以选择使用相同的方法来添加对BufferSerializer<TReaderWriter>.SerializeValue(),如果你愿意,这将使这种类型在INetworkSerializable类型:

    //类名在这里无关紧要。
    public static class SerializationExtensions
    {  
        public static void SerializeValue<TReaderWriter>(this BufferSerializer<TReaderWriter> serializer, ref Url value) where TReaderWriter: IReaderWriter
        {
            if (serializer.IsReader)
            {
                value = new Url();
            }
            serializer.SerializeValue(ref value.Value);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    7 网络对象和网络行为

    GameObjects, NetworkObjectsNetworkBehaviour 不是可序列化的类型,因此不能在 RPC 或者 NetworkVariables 默认情况下。

    有两个方便的wrappers【包装器】可用于发送对NetworkObject或者一个NetworkBehaviour通过RPCNetworkVariables.

    7.1 NetworkObjectReference 网络对象引用类型

    NetworkObjectReference可用于序列化对NetworkObject。它只能在已经生成的上使用NetworkObjects.

    下面是一个使用NetworkObject发送目标的参考NetworkObject通过RPC:

    public class Weapon : NetworkBehaviour
    {
        public void ShootTarget(GameObject target)
        {
            var targetObject = target.GetComponent<NetworkObject>();
            ShootTargetServerRpc(targetObject);
        }
    
        [ServerRpc]
        public void ShootTargetServerRpc(NetworkObjectReference target)
        {
            if (target.TryGet(out NetworkObject targetObject))
            {
                // deal damage or something to target object.
            }
            else
            {
                // Target not found on server, likely because it already has been destroyed/despawned.
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    7.2 Implicit Operators 隐式运算

    还有从/到转换的隐式运算符NetworkObject/GameObject这可以用来简化代码。例如,上述示例也可以用以下方式编写:

    NetworkObjectReference -> NetworkObject
    GameObject -> NetworkObjectReference
    public class Weapon : NetworkBehaviour
    {
        public void ShootTarget(GameObject target)
        {
            ShootTargetServerRpc(target);
        }
    
        [ServerRpc]
        public void ShootTargetServerRpc(NetworkObjectReference target)
        {
            NetworkObject targetObject = target;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    注意: 如果找不到引用,到NetworkObject / GameObject的隐式转换将导致Null。

    7.3 NetworkBehaviourReference 网络行为参考

    NetworkBehaviourReference工作方式类似于NetworkObjectReference而是用来指代特定的NetworkBehaviour衍生的上的组件NetworkObject.

    public class Health : NetworkBehaviour
    {
        public NetworkVariable<int> Health = new NetworkVariable<int>();
    }
    
    public class Weapon : NetworkBehaviour
    {
        public void ShootTarget(GameObject target)
        {
            var health = target.GetComponent<Health>();
            ShootTargetServerRpc(health, 10);
        }
        
    
        [ServerRpc]
        public void ShootTargetServerRpc(NetworkBehaviourReference health, int damage)
        {
            if (health.TryGet(out Health healthComponent))
            {
                healthComponent.Health.Value -= damage;
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    7.4 网络对象引用、网络行为引用的工作原理

    NetworkObjectReferenceNetworkBehaviourReference是方便的wrappers包装器,它序列化NetworkObject当发送时,在接收端检索相应的 用那个id。NetworkBehaviourReference发送一个附加索引,用于查找正确的NetworkBehaviour在哪个NetworkObject上.

    它们都实现INetworkSerializable接口。


    四、 网络变量使用

    网络变量泛型类 NetworkVariable,支持C#基本类型和Unity基本类型。

    1 bool 类型的网络变量使用

    NetworkVariable<T>Value 更改, OnValueChanged 将带有新旧两个值的参数进行回调,回调函数应对值变化而不断轮询最新的值。

    public class Door : NetworkBehaviour
    {
        public NetworkVariable<bool> State = new NetworkVariable<bool>();
    
        public override void OnNetworkSpawn()
        {
            State.OnValueChanged += OnStateChanged;
        }
    
        public override void OnNetworkDespawn()
        {
            State.OnValueChanged -= OnStateChanged;
        }
    
        public void OnStateChanged(bool previous, bool current)
        {
            // note: `State.Value` will be equal to `current` here
            if (State.Value)
            {
                // door is open:
                //  - rotate door transform
                //  - play animations, sound etc.
            }
            else
            {
                // door is closed:
                //  - rotate door transform
                //  - play animations, sound etc.
            }
        }
    
        [ServerRpc(RequireOwnership = false)]
        public void ToggleServerRpc()
        {
            // this will cause a replication over the network
            // and ultimately invoke `OnValueChanged` on receivers
            State.Value = !State.Value;
        }
    }
    
    • 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
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39

    2 网络变量读写权限

    默认情况下,NetworkVariable<T>只能由服务器写并且可以被任何人读取。这些权限可以改变通过构造函数更改。

    // A snippet from the Netcode SDK
    
    public abstract class NetworkVariableBase
    {
        // ...
    
        public const NetworkVariableReadPermission DefaultReadPerm =
            NetworkVariableReadPermission.Everyone;
    
        public const NetworkVariableWritePermission DefaultWritePerm =
            NetworkVariableWritePermission.Server;
    
        // ...
    }
    
    public class NetworkVariable<T> : NetworkVariableBase
    {
        // ...
    
        public NetworkVariable(T value = default,
            NetworkVariableReadPermission readPerm = DefaultReadPerm,
            NetworkVariableWritePermission writePerm = DefaultWritePerm)
        {
            // ...
        }
    
        // ...
    }
    
    • 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

    2.1 Read

    public enum NetworkVariableReadPermission
    {
        Everyone,
        Owner
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    Everyone → All clients and server will get value updates.
    Owner → Only server and the owner client will get value updates.

    2.2 Write

    public enum NetworkVariableWritePermission
    {
        Server,
        Owner
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    Server → Only the server can write to the value.
    Owner → Only the owner client can write to the value, server can’t write to the value.

    3. 示例代码整合

    public class Cube : NetworkBehaviour
    {
        // everyone can read, only owner can write
        public NetworkVariable<Vector3> NetPosition = new NetworkVariable<Vector3>(
            default,
            NetworkVariableBase.DefaultReadPerm, // Everyone
            NetworkVariableWritePermission.Owner);
    
        private void FixedUpdate()
        {
            // owner writes, others read & apply
            if (IsOwner)
            {
                NetPosition.Value = transform.position;
            }
            else
            {
                transform.position = NetPosition.Value;
            }
        }
    
        // everyone can read, only server can write
        public NetworkVariable<Color> NetColor = new NetworkVariable<Color>(
            default,
            NetworkVariableBase.DefaultReadPerm, // Everyone
            NetworkVariableWritePermission.Server);
    
        public override void OnNetworkSpawn()
        {
            NetColor.OnValueChanged += OnColorChanged;
        }
    
        public override void OnNetworkDespawn()
        {
            NetColor.OnValueChanged -= OnColorChanged;
        }
    
        public void OnColorChanged(Color previous, Color current)
        {
            // update materials etc.
        }
    
        [ServerRpc(RequireOwnership = false)]
        public void ChangeColorServerRpc()
        {
            NetColor.Value = Random.ColorHSV();
        }
    }
    
    • 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
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48

    五、 MLAPI pre-10版本的一些变化

    0.0 升级说明

    最近Netcode升级了pre-10版本,官方修复了一些bug,将博主提供的自pre-6版本的自定义泛型网络变量编译导致编辑器崩溃的bug修复了,另外还有一些其他变化。

    在这里插入图片描述
    在这里插入图片描述

    1 修复数组泛型崩溃

    Fixed NetworkAnimator issue where it was not always disposing the NativeArray that is allocated when spawned. (#1946)

    在这里插入图片描述

    2 ClientNetworkTransform 基类函数名 OnIsServerAuthoritatitive 发生变化

    修改前:
    //覆盖此值并返回false以跟随所有者的权限,否则,默认为服务器权限
            protected override bool OnIsServerAuthoritatitive()
            {
                return false;
            }
    修改后:
    //覆盖此值并返回false以跟随所有者的权限,否则,默认为服务器权限
            protected override bool OnIsServerAuthoritative()
            {
                return false;
            }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    3 基类变化:NetworkVariableSerialization修改为NetworkVariableBase

    升级后,部分继承NetworkVariableSerialization自定义网络变量脚本可能会出现报错。
    报错原因是使用了过度阶段使用的基类NetworkVariableSerialization可能准备弃用。

    3.1 报错原因

    1 版本升级前,属于过度脚本,与网络变量一样,继承了网络变量基类NetworkVariableBase,重写ReadDelta等基类方法可以使用封装好的ReadWrite静态方法
    1.1 使用起来比较方便
    ----------------------------------------------------------------------------------------【pre-9】在这里插入图片描述
    2 版本升级后,移除了基类、ReadWrite静态方法等
    2.1 自定义网络变量转回到使用网络变量基类 NetworkVariableBase即可
    ----------------------------------------------------------------------------------------【pre-10】在这里插入图片描述

    3.2 脚本修改示例

    改动前:
     Read(reader, out Value.Array[i]);
     
    改动后:
     reader.ReadValueSafe(out Value.Array[i]);
    
    • 1
    • 2
    • 3
    • 4
    • 5
  • 相关阅读:
    含重复元素取不重复子集[如何取子集?如何去重?]
    Linux文件管理命令
    [红明谷CTF 2021]write_shell %09绕过过滤空格 ``执行
    mysql基于SSM的自习室管理系统毕业设计源码201524
    LFU 缓存 -- LinkedHashSet
    谈思生物医疗直播—可瑞生物CEO谢兴旺博士“TCR创新药的现状和展望”
    matlab实现神经网络算法,人工神经网络matlab代码
    使用TPDSS连接GaussDB数据库
    气象统计方法&短期气候预测代码汇总
    用了一个月的Docker,我真的是爱了
  • 原文地址:https://blog.csdn.net/weixin_38531633/article/details/125520418