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"); 
    }
}

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
    }
}

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
    }
}

4 数组

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

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

[ServerRpc]
void HelloServerRpc(int[] scores, Color[] colors) {
    
     /* ... */ }

[ClientRpc]
void WorldClientRpc(MyComplexType[] values) {
    
     /* ... */ }

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
    }
}

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]);
        }
    }
}

读取:

序列化(反序列化)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);
        }
    }
}

读取:

序列化(反序列化)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);
    }
}

如果我们要单独加载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

也就是说,当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);
    }
}

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);
    }
}

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.
        }
    }
}

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;
    }
}

注意: 如果找不到引用,到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;
        }
    }
}

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;
    }
}

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)
    {
    
    
        // ...
    }

    // ...
}

2.1 Read

public enum NetworkVariableReadPermission
{
    
    
    Everyone,
    Owner
}

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
}

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();
    }
}

五、 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;
        }

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]);

猜你喜欢

转载自blog.csdn.net/weixin_38531633/article/details/125520418