Unity Mirror 从入门到入神(三)

前序文章

Unity Mirror 从入门到入神(三)

在前面我们了解了,Mirror是如何同步生成对象的,接下来我们来看看如何控制生成的对象。在此之前补充一下Command的生效逻辑,以及NetworkBehavior序列化方面的相关知识点,首先来看看Command是如何生效的,我们都知道Command是的调用方向是客户端到服务器,所以理论上来说Hander应该是服务器处理的,顺着这个逻辑找下去就能在NetworkServer.RegisterMessageHandlers找到hander方面的逻辑,至于客户端如何传递参数过来了,我们留到后面再说,先来看看服务器部分是如何分发Command调用的。

另外我猜测Command 在客户端应该是利用了拦截切面之类的能力,来捕获方法执行的

以下代码根据服务端按照代码执行顺序,贴出代码存在删减,如需查看全部代码请访问官网,这里前提假设我们已Host模式启动

NetworkManagerMode

StartHost

    public void StartHost()
{
    if (NetworkServer.active || NetworkClient.active)
    {
        Debug.LogWarning("Server or Client already started.");
        return;
    }

    mode = NetworkManagerMode.Host;

    // StartHost is inherently ASYNCHRONOUS (=doesn't finish immediately)
    //
    // Here is what it does:
    //   Listen
    //   ConnectHost
    //   if onlineScene:
    //       LoadSceneAsync
    //       ...
    //       FinishLoadSceneHost
    //           FinishStartHost
    //               SpawnObjects
    //               StartHostClient      <= not guaranteed to happen after SpawnObjects if onlineScene is set!
    //                   ClientAuth
    //                       success: server sends changescene msg to client
    //   else:
    //       FinishStartHost
    //
    // there is NO WAY to make it synchronous because both LoadSceneAsync
    // and LoadScene do not finish loading immediately. as long as we
    // have the onlineScene feature, it will be asynchronous!

    // setup server first
    SetupServer();

    // scene change needed? then change scene and spawn afterwards.
    // => BEFORE host client connects. if client auth succeeds then the
    //    server tells it to load 'onlineScene'. we can't do that if
    //    server is still in 'offlineScene'. so load on server first.
    if (IsServerOnlineSceneChangeNeeded())
    {
        // call FinishStartHost after changing scene.
        finishStartHostPending = true;
        ServerChangeScene(onlineScene);
    }
    // otherwise call FinishStartHost directly
    else
    {
        FinishStartHost();
    }
}

这…,果然Here is what it does:已经给了所有的关键事件,但是现在我们只关心Command是如何调用的。所以这里只需要关注SetupServer

SetupServer

void SetupServer()
{
    // Debug.Log("NetworkManager SetupServer"); 
    InitializeSingleton();

    // apply settings before initializing anything 一些配置
    NetworkServer.disconnectInactiveConnections = disconnectInactiveConnections;
    NetworkServer.disconnectInactiveTimeout = disconnectInactiveTimeout;
    NetworkServer.exceptionsDisconnect = exceptionsDisconnect;

    if (runInBackground)
        Application.runInBackground = true;

    if (authenticator != null)  
    {
        authenticator.OnStartServer();
        authenticator.OnServerAuthenticated.AddListener(OnServerAuthenticated);
    }

    ConfigureHeadlessFrameRate();
    // start listening to network connections
    NetworkServer.Listen(maxConnections); //启动监听开启网络,并初始化监听方法

    // this must be after Listen(), since that registers the default message handlers
    RegisterServerMessages(); 

    // do not call OnStartServer here yet.
    // this is up to the caller. different for server-only vs. host mode.
}

InitializeSingleton 保持单例,启动之后判断是否是dontDestroyOnLoad,如果是则会进行对应的设置,并把节点强制挂到根目录下,authenticator 链接阶段的鉴权,暂时不关注就当没有,ConfigureHeadlessFrameRate 如果是headless的状态下,会进行锁帧,帧数被设定为NetworkManager.sendRate配置的数值 不管他。重点关注NetworkServer.Listen(maxConnections)该方法包含我们我们要的hander初始化逻辑,RegisterServerMessages服务端的ServerMessage注册,那些属于服务端的ServerMessage,包含以下事件类型。都是Action

  • OnConnectEvent 当客户端连接的时候会发生调用 public static Action<NetworkConnectionToClient> OnConnectedEvent;
  • OnDisconnectedEvent 当客户端断开的时候会发生调用 public static Action<NetworkConnectionToClient> OnDisconnectedEvent;
  • OnErrorEvent 当出现传输异常的室友发生调用 public static Action<NetworkConnectionToClient, TransportError, string> OnErrorEvent;
  • AddPlayerMessage 当客户端发送加入玩家时调用,NetworkServer.OnServerAddPlayer 会被触发
  • ReadyMessage NetworkServer.SetClientReady 生命周期会被触发
    简单了解下即可,暂时不对他们做过多的解读,下面是NetworkServer.Listen的代码

NetworkServer

Listen

        public static void Listen(int maxConns)
        {
            Initialize();
            maxConnections = maxConns;

            // only start server if we want to listen
            if (!dontListen)
            {
                Transport.active.ServerStart();

                if (Transport.active is PortTransport portTransport)
                {
                    if (Utils.IsHeadless())
                    {
#if !UNITY_EDITOR
                        Console.ForegroundColor = ConsoleColor.Green;
                        Console.WriteLine($"Server listening on port {portTransport.Port}");
                        Console.ResetColor();
#else
                        Debug.Log($"Server listening on port {portTransport.Port}");
#endif
                    }
                }
                else
                    Debug.Log("Server started listening");
            }

            active = true;
            RegisterMessageHandlers();
        }

RegisterMessageHandlers

       internal static void RegisterMessageHandlers()
       {
           RegisterHandler<ReadyMessage>(OnClientReadyMessage);
           RegisterHandler<CommandMessage>(OnCommandMessage);
           RegisterHandler<NetworkPingMessage>(NetworkTime.OnServerPing, false);
           RegisterHandler<NetworkPongMessage>(NetworkTime.OnServerPong, false);
           RegisterHandler<EntityStateMessage>(OnEntityStateMessage, true);
           RegisterHandler<TimeSnapshotMessage>(OnTimeSnapshotMessage, true);
       }

终于看到RegisterHandler对应的逻辑了,这里注册了,生命周期事件,CommandMessage 就是在这里进行注册的,接着来看下具体的回调方法逻辑,另外简单介绍下其他的几个事件

  • ReadyMessage 同上,在RegisterServerMessages 被覆盖了
  • CommandMessage 接收来之客户端的Command调用指令
  • NetworkPingMessage 字面意思
  • NetworkPongMessage 字面意思,Mirror有自己的检测客户端是否在线的pingPong机制,不管他
  • EntityStateMessage NetworkBehavior 数值同步暂时不管他
  • TimeSnapshotMessage 时间快照暂时不管他

OnCommandMessage

static void OnCommandMessage(NetworkConnectionToClient conn, CommandMessage msg, int channelId)
{
    if (!conn.isReady)
    {
        // Clients may be set NotReady due to scene change or other game logic by user, e.g. respawning.
        // Ignore commands that may have been in flight before client received NotReadyMessage message.
        // Unreliable messages may be out of order, so don't spam warnings for those.
        if (channelId == Channels.Reliable)
        {
            // Attempt to identify the target object, component, and method to narrow down the cause of the error.
            if (spawned.TryGetValue(msg.netId, out NetworkIdentity netIdentity))
                if (msg.componentIndex < netIdentity.NetworkBehaviours.Length && netIdentity.NetworkBehaviours[msg.componentIndex] is NetworkBehaviour component)
                    if (RemoteProcedureCalls.GetFunctionMethodName(msg.functionHash, out string methodName))
                    {
                        Debug.LogWarning($"Command {methodName} received for {netIdentity.name} [netId={msg.netId}] component {component.name} [index={msg.componentIndex}] when client not ready.\nThis may be ignored if client intentionally set NotReady.");
                        return;
                    }

            Debug.LogWarning("Command received while client is not ready.\nThis may be ignored if client intentionally set NotReady.");
        }
        return;
    }

    if (!spawned.TryGetValue(msg.netId, out NetworkIdentity identity))
    {
        // over reliable channel, commands should always come after spawn.
        // over unreliable, they might come in before the object was spawned.
        // for example, NetworkTransform.
        // let's not spam the console for unreliable out of order messages.
        if (channelId == Channels.Reliable)
            Debug.LogWarning($"Spawned object not found when handling Command message {identity.name} netId={msg.netId}");
        return;
    }

    // Commands can be for player objects, OR other objects with client-authority
    // -> so if this connection's controller has a different netId then
    //    only allow the command if clientAuthorityOwner
    bool requiresAuthority = RemoteProcedureCalls.CommandRequiresAuthority(msg.functionHash);
    if (requiresAuthority && identity.connectionToClient != conn)
    {
        // Attempt to identify the component and method to narrow down the cause of the error.
        if (msg.componentIndex < identity.NetworkBehaviours.Length && identity.NetworkBehaviours[msg.componentIndex] is NetworkBehaviour component)
            if (RemoteProcedureCalls.GetFunctionMethodName(msg.functionHash, out string methodName))
            {
                Debug.LogWarning($"Command {methodName} received for {identity.name} [netId={msg.netId}] component {component.name} [index={msg.componentIndex}] without authority");
                return;
            }

        Debug.LogWarning($"Command received for {identity.name} [netId={msg.netId}] without authority");
        return;
    }

    // Debug.Log($"OnCommandMessage for netId:{msg.netId} conn:{conn}");

    using (NetworkReaderPooled networkReader = NetworkReaderPool.Get(msg.payload))
        identity.HandleRemoteCall(msg.componentIndex, msg.functionHash, RemoteCallType.Command, networkReader, conn);
}

首先确保连接进入的客户端connet处于ready状态,否则直接返回并打印警告异常,判断当前msg.functionHash是否需要权限,这里functionHash的生成逻辑之前也提到过

RemoteCall.cs

CommandRequiresAuthority

       internal static bool CommandRequiresAuthority(ushort cmdHash) =>
           GetInvokerForHash(cmdHash, RemoteCallType.Command, out Invoker invoker) &&
           invoker.cmdRequiresAuthority;

invoker是 从 static readonly Dictionary<ushort, Invoker> remoteCallDelegates = new Dictionary<ushort, Invoker>();委托字典中取出,后面再来确认Command是如何将对应的方法信息放入到remoteCallDelegates中的,这里只需要知道,remoteCallDelegates放了所有的Command方法的Hash字典即可,还记得我们之前提到的例子中有 [Command(reuiresAuthority=false)]这里的即是设置是否需要权限,如果需权限的情况下,则必须满足调用的客户端conn是owner的前提否则直接return,并打印相关日志信息。

    using (NetworkReaderPooled networkReader = NetworkReaderPool.Get(msg.payload))
        identity.HandleRemoteCall(msg.componentIndex, msg.functionHash, RemoteCallType.Command, networkReader, conn);

msg.payload为具体的参数序列化的二进制表达形式,利用networkReader可以将数据按照指定的类型按照规则读取出来,

NetworkIdentity

HandleRemoteCall

 internal void HandleRemoteCall(byte componentIndex, ushort functionHash, RemoteCallType remoteCallType, NetworkReader reader, NetworkConnectionToClient senderConnection = null)
 {
     // check if unity object has been destroyed
     if (this == null)
     {
         Debug.LogWarning($"{remoteCallType} [{functionHash}] received for deleted object [netId={netId}]");
         return;
     }

     // find the right component to invoke the function on
     if (componentIndex >= NetworkBehaviours.Length)
     {
         Debug.LogWarning($"Component [{componentIndex}] not found for [netId={netId}]");
         return;
     }

     NetworkBehaviour invokeComponent = NetworkBehaviours[componentIndex];
     if (!RemoteProcedureCalls.Invoke(functionHash, remoteCallType, reader, invokeComponent, senderConnection))
     {
         Debug.LogError($"Found no receiver for incoming {remoteCallType} [{functionHash}] on {gameObject.name}, the server and client should have the same NetworkBehaviour instances [netId={netId}].");
     }
 }

identity是通过msg.netId中从spawned中获取的,HandleRemoteCall 应该是一个[ClientRPC]``[Command] 共用的方法,才会有RemoteCallType类型需要传递,了解完Command的流程基本ClientRPC就差不多知道了,两者的差别是数据的传输方向不同,一个是客户端到服务器,一个是服务器到客户端。RemoteProcedureCalls.Invoke直接从remoteCallDelegates中拿到Action然后直接执行,至此我们跟踪完了Command生效的调用逻辑

RemoteCall.cs

Invoke

internal static bool Invoke(ushort functionHash, RemoteCallType remoteCallType, NetworkReader reader, NetworkBehaviour component, NetworkConnectionToClient senderConnection = null)
{
    // IMPORTANT: we check if the message's componentIndex component is
    //            actually of the right type. prevents attackers trying
    //            to invoke remote calls on wrong components.
    if (GetInvokerForHash(functionHash, remoteCallType, out Invoker invoker) &&
        invoker.componentType.IsInstanceOfType(component))
    {
        // invoke function on this component
        invoker.function(component, reader, senderConnection);
        return true;
    }
    return false;
}

RegisterCommand RegisterRpc

接下来我们来看看这些Action是如何注册到RemoteCall.remoteCallDelegates中的,通过查看RemoteCalls.cs中的源代码,发现了两个注册ClientRPC,以及Comand的两个方法

        // pass full function name to avoid ClassA.Func <-> ClassB.Func collisions
        // need to pass componentType to support invoking on GameObjects with
        // multiple components of same type with same remote call.
        public static void RegisterCommand(Type componentType, string functionFullName, RemoteCallDelegate func, bool requiresAuthority) =>
            RegisterDelegate(componentType, functionFullName, RemoteCallType.Command, func, requiresAuthority);

        // pass full function name to avoid ClassA.Func <-> ClassB.Func collisions
        // need to pass componentType to support invoking on GameObjects with
        // multiple components of same type with same remote call.
        public static void RegisterRpc(Type componentType, string functionFullName, RemoteCallDelegate func) =>
            RegisterDelegate(componentType, functionFullName, RemoteCallType.ClientRpc, func);

RegisterDelegate

    internal static ushort RegisterDelegate(Type componentType, string functionFullName, RemoteCallType remoteCallType, RemoteCallDelegate func, bool cmdRequiresAuthority = true)
    {
        // type+func so Inventory.RpcUse != Equipment.RpcUse
        ushort hash = (ushort)(functionFullName.GetStableHashCode() & 0xFFFF);

        if (CheckIfDelegateExists(componentType, remoteCallType, func, hash))
            return hash;

        remoteCallDelegates[hash] = new Invoker
        {
            callType = remoteCallType,
            componentType = componentType,
            function = func,
            cmdRequiresAuthority = cmdRequiresAuthority
        };
        return hash;
    }

以上就是将指定的远程方法注册到remoteCallDelegates的具体实现,那是哪里调用了 RegisterCommand``RegisterRpc,通过visual studio查看引用,发现并没有任何地方对该方法进行调用,所有我们以该关键字进行全局搜索看看,顺着关键字源头往上找,找到了ILProcessorHook.cs中Process方法的调用来源。 public class ILPostProcessorHook : ILPostProcessor该类继承至一个叫ILPostProcessor的接口,程序集属于Unity.CompilationPipeline.Common。看名字大概就能知道是Unity编译过曾中的Hook钩子回调了,没学过Unity靠名字八九不离十吧,注释这里有解释

ILPostProcessorHook

     // ILPostProcessor is invoked by Unity.
     // we can not tell it to ignore certain assemblies before processing.
     // add a 'ignore' define for convenience.
     // => WeaverTests/WeaverAssembler need it to avoid Unity running it
    public const string IgnoreDefine = "ILPP_IGNORE";

Hook有两个主要的方法,一个是WillProcess判断当前的compiledAssembly 是否符合拦截标准,不符合的情况下直接跳过,

WillProcess

    // from CompilationFinishedHook
    const string MirrorRuntimeAssemblyName = "Mirror";

    // ILPostProcessor is invoked by Unity.
    // we can not tell it to ignore certain assemblies before processing.
    // add a 'ignore' define for convenience.
    // => WeaverTests/WeaverAssembler need it to avoid Unity running it
    public const string IgnoreDefine = "ILPP_IGNORE";
    // check if assembly has the 'ignore' define
    static bool HasDefine(ICompiledAssembly assembly, string define) =>
        assembly.Defines != null &&
        assembly.Defines.Contains(define);
   public override bool WillProcess(ICompiledAssembly compiledAssembly)
   {
       // compiledAssembly.References are file paths:
       //   Library/Bee/artifacts/200b0aE.dag/Mirror.CompilerSymbols.dll
       //   Assets/Mirror/Plugins/Mono.Cecil/Mono.CecilX.dll
       //   /Applications/Unity/Hub/Editor/2021.2.0b6_apple_silicon/Unity.app/Contents/NetStandard/ref/2.1.0/netstandard.dll
       //
       // log them to see:
       //     foreach (string reference in compiledAssembly.References)
       //         LogDiagnostics($"{compiledAssembly.Name} references {reference}");
       bool relevant = compiledAssembly.Name == MirrorRuntimeAssemblyName ||
                       compiledAssembly.References.Any(filePath => Path.GetFileNameWithoutExtension(filePath) == MirrorRuntimeAssemblyName);
       bool ignore = HasDefine(compiledAssembly, IgnoreDefine);
       return relevant && !ignore;
   }

另外一个方法是Process主要的处理逻辑就包含在这里

Process

public override ILPostProcessResult Process(ICompiledAssembly compiledAssembly)
{
    //Log.Warning($"Processing {compiledAssembly.Name}");

    // load the InMemoryAssembly peData into a MemoryStream
    byte[] peData = compiledAssembly.InMemoryAssembly.PeData;
    //LogDiagnostics($"  peData.Length={peData.Length} bytes");
    using (MemoryStream stream = new MemoryStream(peData))
    using (ILPostProcessorAssemblyResolver asmResolver = new ILPostProcessorAssemblyResolver(compiledAssembly, Log))
    {
        // we need to load symbols. otherwise we get:
        // "(0,0): error Mono.CecilX.Cil.SymbolsNotFoundException: No symbol found for file: "
        using (MemoryStream symbols = new MemoryStream(compiledAssembly.InMemoryAssembly.PdbData))
        {
            ReaderParameters readerParameters = new ReaderParameters{
                SymbolStream = symbols,
                ReadWrite = true,
                ReadSymbols = true,
                AssemblyResolver = asmResolver,
                // custom reflection importer to fix System.Private.CoreLib
                // not being found in custom assembly resolver above.
                ReflectionImporterProvider = new ILPostProcessorReflectionImporterProvider()
            };
            using (AssemblyDefinition asmDef = AssemblyDefinition.ReadAssembly(stream, readerParameters))
            {
                // resolving a Mirror.dll type like NetworkServer while
                // weaving Mirror.dll does not work. it throws a
                // NullReferenceException in WeaverTypes.ctor
                // when Resolve() is called on the first Mirror type.
                // need to add the AssemblyDefinition itself to use.
                asmResolver.SetAssemblyDefinitionForCompiledAssembly(asmDef);

                // weave this assembly.
                Weaver weaver = new Weaver(Log);
                if (weaver.Weave(asmDef, asmResolver, out bool modified))
                {
                    //Log.Warning($"Weaving succeeded for: {compiledAssembly.Name}");

                    // write if modified
                    if (modified)
                    {
                        // when weaving Mirror.dll with ILPostProcessor,
                        // Weave() -> WeaverTypes -> resolving the first
                        // type in Mirror.dll adds a reference to
                        // Mirror.dll even though we are in Mirror.dll.
                        // -> this would throw an exception:
                        //    "Mirror references itself" and not compile
                        // -> need to detect and fix manually here
                        if (asmDef.MainModule.AssemblyReferences.Any(r => r.Name == asmDef.Name.Name))
                        {
                            asmDef.MainModule.AssemblyReferences.Remove(asmDef.MainModule.AssemblyReferences.First(r => r.Name == asmDef.Name.Name));
                            //Log.Warning($"fixed self referencing Assembly: {asmDef.Name.Name}");
                        }

                        MemoryStream peOut = new MemoryStream();
                        MemoryStream pdbOut = new MemoryStream();
                        WriterParameters writerParameters = new WriterParameters
                        {
                            SymbolWriterProvider = new PortablePdbWriterProvider(),
                            SymbolStream = pdbOut,
                            WriteSymbols = true
                        };

                        asmDef.Write(peOut, writerParameters);

                        InMemoryAssembly inMemory = new InMemoryAssembly(peOut.ToArray(), pdbOut.ToArray());
                        return new ILPostProcessResult(inMemory, Log.Logs);
                    }
                }
                // if anything during Weave() fails, we log an error.
                // don't need to indicate 'weaving failed' again.
                // in fact, this would break tests only expecting certain errors.
                //else Log.Error($"Weaving failed for: {compiledAssembly.Name}");
            }
        }
    }

    // always return an ILPostProcessResult with Logs.
    // otherwise we won't see Logs if weaving failed.
    return new ILPostProcessResult(compiledAssembly.InMemoryAssembly, Log.Logs);
}

要读懂上面的代码之前我们先对ILPost方面的相关类熟悉一下,代码丢给C老师,大概能知道是一个处理中间代码的东西,类似于字节码?遇到几个核心类这里进行补充说明

InMemoryAssembly

    public class InMemoryAssembly
    {
        public byte[] PeData { get; set; }

        public byte[] PdbData { get; set; }

        public InMemoryAssembly(byte[] peData, byte[] pdbData)
        {
            PeData = peData;
            PdbData = pdbData;
        }
    }
  • PeData: 这个属性用来存储程序集(通常是 .dll 或 .exe 文件)的二进制数据。PE (Portable Executable) 是 Windows 可执行文件格式的标准缩写。
  • PdbData: 这个属性用来存储调试符号数据,通常是 .pdb (Program Database) 文件中的数据。这些调试符号数据在调试时非常有用,可以映射二进制代码到源代码。

ICompiledAssembly

    public interface ICompiledAssembly
    {
        InMemoryAssembly InMemoryAssembly { get; }

        string Name { get; }

        string[] References { get; }

        string[] Defines { get; }
    }

在Unity中我们可以在某一个目录下新建一个程序集,并且设置程序集的名称,以及依赖关系,此时References中即包含对其他程序集的引用关系,Name就是程序集的名称,Defines中包含了条件编译符号

TypeReference

类定义,不必深究,知道是描述描述一个class编译之后的信息就可以

MethodReference

方法定义 不必深究,知道其中包含了一个方法的 方法名称,修饰符,返回类型,泛型,参数,以及方法体指令即可

ParameterDefinition

参数定义 不必深究

ILProcessor

方法体定义 该类型通过md.Body.GetILProcessor();获取,其中包含了一个方法的所有OPCode指令信息,可以理解为字节码集合,因为IL2CPP是基于编译之后的字节码再优化编译为本地机器码,所以mirror并没有破坏il2cpp的支持。不过对于方法重载需要注意,比如Test(int i),Test(string i),需要写成TestInt,TestString

下面的内容会有些烧脑,对于第一次接触,Mono.CecilX的同学来说会有些困难,但是只要知道这个东西可以动态编辑字节码,是一个用于读取、编写和修改 .NET 程序集的库就可以了。Process方法主要做的事情就是扫描程序集中的所有类,检查Attribute的使用是否正确,比如SyncVar需要在NetworkBehavior中使用,扫描Client,TargetRPC,Command,Server 修改原有的方法,或生成新增的方法进行替换,达到从方法层面切面的目的。
比如Client和Server这两个注解明确表示在客户端执行,在服务端执行,在编译阶段,会在ILProcessor的头部,可以理解为第一行代码处,新增指令,判断当前NetworkClient.active和NetworkServer.active 是否启用,对于ClientRPC 和Command TargetRpc,(只看了Command其他进行了类推)Mirror会新增一个前缀方法将原有方法进行替换,原有方法会变成,调用SendCommandInternal(string functionFullName, int functionHashCode, NetworkWriter writer, int channelId, bool requiresAuthority = true)这个是所有的NetworkBehavior类都有的方法,新增的方法以 $"USER_CODE_{MethodName}"这样的名字作为方法名,然后这里有个细节就是在生成之后会将新生成方法的,内部的递归调用修改为调用新方法,在Command方法内递归调用Command方法并不会持续的触发远程调用,如果在Command方法中调用其他方法,这里将会执行原有方法的调用逻辑,以下是替换生成代码的相关逻辑Command部分

    // weave this assembly.
    Weaver weaver = new Weaver(Log);

整个Mirror的序列化和修改字节码都依赖这个Weaver模块,网上搜发现并灭有其他的信息,这个应该是Mirror作者自己取得一个名字主要得逻辑部分在Weaver.weaver中if (weaver.Weave(asmDef, asmResolver, out bool modified))

Weaver

weaver

// Weave takes an AssemblyDefinition to be compatible with both old and
// new weavers:
// * old takes a filepath, new takes a in-memory byte[]
// * old uses DefaultAssemblyResolver with added dependencies paths,
//   new uses ...?
//
// => assembly: the one we are currently weaving (MyGame.dll)
// => resolver: useful in case we need to resolve any of the assembly's
//              assembly.MainModule.AssemblyReferences.
//              -> we can resolve ANY of them given that the resolver
//                 works properly (need custom one for ILPostProcessor)
//              -> IMPORTANT: .Resolve() takes an AssemblyNameReference.
//                 those from assembly.MainModule.AssemblyReferences are
//                 guaranteed to be resolve-able.
//                 Parsing from a string for Library/.../Mirror.dll
//                 would not be guaranteed to be resolve-able because
//                 for ILPostProcessor we can't assume where Mirror.dll
//                 is etc.
public bool Weave(AssemblyDefinition assembly, IAssemblyResolver resolver, out bool modified)
{
    WeavingFailed = false;
    modified = false;
    try
    {
        CurrentAssembly = assembly;

        // fix "No writer found for ..." error
        // https://github.com/vis2k/Mirror/issues/2579
        // -> when restarting Unity, weaver would try to weave a DLL
        //    again
        // -> resulting in two GeneratedNetworkCode classes (see ILSpy)
        // -> the second one wouldn't have all the writer types setup
        if (CurrentAssembly.MainModule.ContainsClass(GeneratedCodeNamespace, GeneratedCodeClassName))
        {
            //Log.Warning($"Weaver: skipping {CurrentAssembly.Name} because already weaved");
            return true;
        }

        weaverTypes = new WeaverTypes(CurrentAssembly, Log, ref WeavingFailed);

        // weaverTypes are needed for CreateGeneratedCodeClass
        CreateGeneratedCodeClass();

        // WeaverList depends on WeaverTypes setup because it uses Import
        syncVarAccessLists = new SyncVarAccessLists();

        // initialize readers & writers with this assembly.
        // we need to do this in every Process() call.
        // otherwise we would get
        // "System.ArgumentException: Member ... is declared in another module and needs to be imported"
        // errors when still using the previous module's reader/writer funcs.
        writers = new Writers(CurrentAssembly, weaverTypes, GeneratedCodeClass, Log);
        readers = new Readers(CurrentAssembly, weaverTypes, GeneratedCodeClass, Log);

        Stopwatch rwstopwatch = Stopwatch.StartNew();
        // Need to track modified from ReaderWriterProcessor too because it could find custom read/write functions or create functions for NetworkMessages
        modified = ReaderWriterProcessor.Process(CurrentAssembly, resolver, Log, writers, readers, ref WeavingFailed);
        rwstopwatch.Stop();
        Console.WriteLine($"Find all reader and writers took {rwstopwatch.ElapsedMilliseconds} milliseconds");

        ModuleDefinition moduleDefinition = CurrentAssembly.MainModule;
        Console.WriteLine($"Script Module: {moduleDefinition.Name}");

        modified |= WeaveModule(moduleDefinition);

        if (WeavingFailed)
        {
            return false;
        }

        if (modified)
        {
            SyncVarAttributeAccessReplacer.Process(Log, moduleDefinition, syncVarAccessLists);

            // add class that holds read/write functions
            moduleDefinition.Types.Add(GeneratedCodeClass);

            ReaderWriterProcessor.InitializeReaderAndWriters(CurrentAssembly, weaverTypes, writers, readers, GeneratedCodeClass);

            // DO NOT WRITE here.
            // CompilationFinishedHook writes to the file.
            // ILPostProcessor writes to in-memory assembly.
            // it depends on the caller.
            //CurrentAssembly.Write(new WriterParameters{ WriteSymbols = true });
        }

        // if weaving succeeded, switch on the Weaver Fuse in Mirror.dll
        if (CurrentAssembly.Name.Name == MirrorAssemblyName)
        {
            ToggleWeaverFuse();
        }

        return true;
    }
    catch (Exception e)
    {
        Log.Error($"Exception :{e}");
        WeavingFailed = true;
        return false;
    }
}

WeaverTypes

weaverTypes = new WeaverTypes(CurrentAssembly, Log, ref WeavingFailed);WeaverTypes 包含了可能用得methodReference,它得主要目的是,将后续修改生成代码中需要引用调用的method全部收集起来,使用使用类成员对象的形式进行引用,因为ILPostProcessor是多线程环境,所以该类是非静态实例,主要使用到了 Mono.CecilX的两个方法,一个找类一个找方法,非静态构造函数,和静态构造函数也是方法,静态构造函数(.cctor)在类加载阶段执行用于初始化静态类对象,非静态构造方法(.ctor)在,new实例化的时候执行,用于初始化类成员对象。

  
    TypeReference ArraySegmentType = Import(typeof(ArraySegment<>));
    ArraySegmentConstructorReference = Resolvers.ResolveMethod(ArraySegmentType, assembly, Log, ".ctor", ref WeavingFailed);

    TypeReference ActionType = Import(typeof(Action<,>));
    ActionT_T = Resolvers.ResolveMethod(ActionType, assembly, Log, ".ctor", ref WeavingFailed);

  public TypeReference ImportReference(Type type, IGenericParameterProvider context)
 {
     Mixin.CheckType(type);
     CheckContext(context, this);
     return ReflectionImporter.ImportReference(type, context);
 }

 public MethodReference ImportReference(MethodReference method, IGenericParameterProvider context)
 {
     Mixin.CheckMethod(method);
     if (method.Module == this)
     {
         return method;
     }

     CheckContext(context, this);
     return MetadataImporter.ImportReference(method, context);
 }

下面我们会看到在编织环境,会利用OPCodes.Call的指令调用= Resolvers.ResolveMethod(NetworkBehaviourType, assembly, Log, "SendCommandInternal", ref WeavingFailed);这部分的逻辑,就是通过weaverTypes.sendCommandInternal 的形式拿到的

// weaverTypes are needed for CreateGeneratedCodeClass
CreateGeneratedCodeClass();

这一行实在新建一个名字叫做Mirror.GeneratedNetworkCode的类,动态构建的,作用暂时不管,

 // WeaverList depends on WeaverTypes setup because it uses Import
 syncVarAccessLists = new SyncVarAccessLists();

syncVarAccessLists一个临时存储SyncVar 生成的Setter和Getter方法,以及统计对应的class中SyncVar的数量,下面是对应的源代码

// setter functions that replace [SyncVar] member variable references. dict<field, replacement>
public Dictionary<FieldDefinition, MethodDefinition> replacementSetterProperties =
    new Dictionary<FieldDefinition, MethodDefinition>();

// getter functions that replace [SyncVar] member variable references. dict<field, replacement>
public Dictionary<FieldDefinition, MethodDefinition> replacementGetterProperties =
    new Dictionary<FieldDefinition, MethodDefinition>();

// amount of SyncVars per class. dict<className, amount>
// necessary for SyncVar dirty bits, where inheriting classes start
// their dirty bits at base class SyncVar amount.
public Dictionary<string, int> numSyncVars = new Dictionary<string, int>();
// initialize readers & writers with this assembly.
// we need to do this in every Process() call.
// otherwise we would get
// "System.ArgumentException: Member ... is declared in another module and needs to be imported"
// errors when still using the previous module's reader/writer funcs.
writers = new Writers(CurrentAssembly, weaverTypes, GeneratedCodeClass, Log);
readers = new Readers(CurrentAssembly, weaverTypes, GeneratedCodeClass, Log);

Writers Readers提供了各种类型的序列化方法,包含MessageStruct的enum,float,int等基础类型,Array,NetworkBehavior,主要管理的是对象序列化的能力,同理Readers主要管理的是对象反序列化的能力,这中间很好理解,比如序列化一个struct,我就可以根据struct的fields,判断类型在通过Type从Writers中获取Writer方法,直到可以直接写入到byte[]中为止,比如float,int这些值类型,直接通过writerBytes进行序列化,基础类型的序列化能力由NetworkWriter提供,同理Readers和NetworkReader,另外Writers中会把生成的序列化静态函数添加到 Mirror.GeneratedNetworkCode,拿一些需要添加是在扫描程序集的过程中懒加载的,遇到了才会调用Wirters和Readers初始化。

modified = ReaderWriterProcessor.Process(CurrentAssembly, resolver, Log, writers, readers, ref WeavingFailed)

这个方法就是核心的程序集扫描处理进程了,

ReaderWriterProcessor

Process

    public static bool Process(AssemblyDefinition CurrentAssembly, IAssemblyResolver resolver, Logger Log, Writers writers, Readers readers, ref bool WeavingFailed)
    {
        // find NetworkReader/Writer extensions from Mirror.dll first.
        // and NetworkMessage custom writer/reader extensions.
        // NOTE: do not include this result in our 'modified' return value,
        //       otherwise Unity crashes when running tests
        ProcessMirrorAssemblyClasses(CurrentAssembly, resolver, Log, writers, readers, ref WeavingFailed);

        // find readers/writers in the assembly we are in right now.
        return ProcessAssemblyClasses(CurrentAssembly, CurrentAssembly, writers, readers, ref WeavingFailed);
    }

先处理Mirror自身的,再处理用户的,逻辑基本上是一致的,在ProcessMirrorAssemblyClasses中又调用了ProcessAssemblyClasses,只是把resolver替换成了Mirror包的自己的

ProcessAssemblyClasses

    static bool ProcessAssemblyClasses(AssemblyDefinition CurrentAssembly, AssemblyDefinition assembly, Writers writers, Readers readers, ref bool WeavingFailed)
    {
        bool modified = false;
        foreach (TypeDefinition klass in assembly.MainModule.Types)
        {
            // extension methods only live in static classes
            // static classes are represented as sealed and abstract
            if (klass.IsAbstract && klass.IsSealed)
            {
                // if assembly has any declared writers then it is "modified"
                modified |= LoadDeclaredWriters(CurrentAssembly, klass, writers);
                modified |= LoadDeclaredReaders(CurrentAssembly, klass, readers);
            }
        }

        foreach (TypeDefinition klass in assembly.MainModule.Types)
        {
            // if assembly has any network message then it is modified
            modified |= LoadMessageReadWriter(CurrentAssembly.MainModule, writers, readers, klass, ref WeavingFailed);
        }
        return modified;
    }

可以看到assembly被用于提供所有的types,即类

if (klass.IsAbstract && klass.IsSealed)
{
    // if assembly has any declared writers then it is "modified"
    modified |= LoadDeclaredWriters(CurrentAssembly, klass, writers);
    modified |= LoadDeclaredReaders(CurrentAssembly, klass, readers);
}

LoadDeclaredWriters

static bool LoadDeclaredWriters(AssemblyDefinition currentAssembly, TypeDefinition klass, Writers writers)
{
    // register all the writers in this class.  Skip the ones with wrong signature
    bool modified = false;
    foreach (MethodDefinition method in klass.Methods)
    {
        if (method.Parameters.Count != 2)
            continue;

        if (!method.Parameters[0].ParameterType.Is<NetworkWriter>())
            continue;

        if (!method.ReturnType.Is(typeof(void)))
            continue;

        if (!method.HasCustomAttribute<System.Runtime.CompilerServices.ExtensionAttribute>())
            continue;

        if (method.HasGenericParameters)
            continue;

        TypeReference dataType = method.Parameters[1].ParameterType;
        writers.Register(dataType, currentAssembly.MainModule.ImportReference(method));
        modified = true;
    }
    return modified;
}

LoadDeclaredReaders

static bool LoadDeclaredReaders(AssemblyDefinition currentAssembly, TypeDefinition klass, Readers readers)
{
    // register all the reader in this class.  Skip the ones with wrong signature
    bool modified = false;
    foreach (MethodDefinition method in klass.Methods)
    {
        if (method.Parameters.Count != 1)
            continue;

        if (!method.Parameters[0].ParameterType.Is<NetworkReader>())
            continue;

        if (method.ReturnType.Is(typeof(void)))
            continue;

        if (!method.HasCustomAttribute<System.Runtime.CompilerServices.ExtensionAttribute>())
            continue;

        if (method.HasGenericParameters)
            continue;

        readers.Register(method.ReturnType, currentAssembly.MainModule.ImportReference(method));
        modified = true;
    }
    return modified;
}

这段代码的目的是从所有的静态类中扫描扩展方法,在C#中我们可以对某一个类型的方法进行拓展,比如之前提到的GetMethodStableHashCode

 public static ushort GetStableHashCode16(this string text)
 {
     // deterministic hash
     int hash = GetStableHashCode(text);

     // Gets the 32bit fnv1a hash
     // To get it down to 16bit but still reduce hash collisions we cant just cast it to ushort
     // Instead we take the highest 16bits of the 32bit hash and fold them with xor into the lower 16bits
     // This will create a more uniform 16bit hash, the method is described in:
     // http://www.isthe.com/chongo/tech/comp/fnv/ in section "Changing the FNV hash size - xor-folding"
     return (ushort)((hash >> 16) ^ hash);
 }

在C#中提供了一种机制允许扩展指定类型的方法,扩展方法必须定义在静态类中,且第一个参数必须使用 this 关键字指定要扩展的类型。编译器在编译扩展方法时会自动为这些方法添加 ExtensionAttribute 属性。所以Mirror中我们可以很容易对NetworkWriter,NetworkRead中新增自定义的序列化方法,Mirror把对NetworkWriter的扩展方法同一放置到了 在Mirror.NetworkWriterExtensions.cs这个静态类中,我们找几个例子看看

NetworkWriterExtensions

WriteGameObject

     public static void WriteGameObject(this NetworkWriter writer, GameObject value)
     {
         if (value == null)
         {
             writer.WriteUInt(0);
             return;
         }

         // warn if the GameObject doesn't have a NetworkIdentity,
         if (!value.TryGetComponent(out NetworkIdentity identity))
             Debug.LogWarning($"Attempted to sync a GameObject ({value}) which isn't networked. GameObject without a NetworkIdentity component can't be synced.");

         // serialize the correct amount of data in any case to make sure
         // that the other end can read the expected amount of data too.
         writer.WriteNetworkIdentity(identity);
     }

序列化一个GameObject,实际上序列化组件identity

WriteNetworkIdentity

public static void WriteNetworkIdentity(this NetworkWriter writer, NetworkIdentity value)
{
    if (value == null)
    {
        writer.WriteUInt(0);
        return;
    }

    // users might try to use unspawned / prefab GameObjects in
    // rpcs/cmds/syncvars/messages. they would be null on the other
    // end, and it might not be obvious why. let's make it obvious.
    // https://github.com/vis2k/Mirror/issues/2060
    //
    // => warning (instead of exception) because we also use a warning
    //    if a GameObject doesn't have a NetworkIdentity component etc.
    if (value.netId == 0)
        Debug.LogWarning($"Attempted to serialize unspawned GameObject: {value.name}. Prefabs and unspawned GameObjects would always be null on the other side. Please spawn it before using it in [SyncVar]s/Rpcs/Cmds/NetworkMessages etc.");

    writer.WriteUInt(value.netId);
}

而identity的序列化实际上是写入netId,如何此看来,我们完全可以在Command ClientRPC中直接传递对应的GameObj,只要GameObj上刮了Identity组件

WriteNetworkBehaviour

 public static void WriteNetworkBehaviour(this NetworkWriter writer, NetworkBehaviour value)
 {
     if (value == null)
     {
         writer.WriteUInt(0);
         return;
     }

     // users might try to use unspawned / prefab NetworkBehaviours in
     // rpcs/cmds/syncvars/messages. they would be null on the other
     // end, and it might not be obvious why. let's make it obvious.
     // https://github.com/vis2k/Mirror/issues/2060
     // and more recently https://github.com/MirrorNetworking/Mirror/issues/3399
     //
     // => warning (instead of exception) because we also use a warning
     //    when writing an unspawned NetworkIdentity
     if (value.netId == 0)
     {
         Debug.LogWarning($"Attempted to serialize unspawned NetworkBehaviour: of type {value.GetType()} on GameObject {value.name}. Prefabs and unspawned GameObjects would always be null on the other side. Please spawn it before using it in [SyncVar]s/Rpcs/Cmds/NetworkMessages etc.");
         writer.WriteUInt(0);
         return;
     }

     writer.WriteUInt(value.netId);
     writer.WriteByte(value.ComponentIndex);
 }

只要是继承或实例化NetworkBehaviour的序列化,序列化方式为找到identity拿到netId,并将当前NetworkBehavior的ComponentIndex一并写入用于定位

比较有趣的是Mirror竟然可以序列化Sprite

WriteSprite

   public static void WriteSprite(this NetworkWriter writer, Sprite sprite)
   {
       // support 'null' textures for [SyncVar]s etc.
       // https://github.com/vis2k/Mirror/issues/3144
       // simply send a 'null' for texture content.
       if (sprite == null)
       {
           writer.WriteTexture2D(null);
           return;
       }

       writer.WriteTexture2D(sprite.texture);
       writer.WriteRect(sprite.rect);
       writer.WriteVector2(sprite.pivot);
   }

WriteTexture2D

public static void WriteTexture2D(this NetworkWriter writer, Texture2D texture2D)
{
    // TODO allocation protection when sending textures to server.
    //      currently can allocate 32k x 32k x 4 byte = 3.8 GB

    // support 'null' textures for [SyncVar]s etc.
    // https://github.com/vis2k/Mirror/issues/3144
    // simply send -1 for width.
    if (texture2D == null)
    {
        writer.WriteShort(-1);
        return;
    }

    // check if within max size, otherwise Reader can't read it.
    int totalSize = texture2D.width * texture2D.height;
    if (totalSize > NetworkReader.AllocationLimit)
        throw new IndexOutOfRangeException($"NetworkWriter.WriteTexture2D - Texture2D total size (width*height) too big: {totalSize}. Limit: {NetworkReader.AllocationLimit}");

    // write dimensions first so reader can create the texture with size
    // 32k x 32k short is more than enough
    writer.WriteShort((short)texture2D.width);
    writer.WriteShort((short)texture2D.height);
    writer.WriteArray(texture2D.GetPixels32());
}

可以看到他直接texture2D中的所有像素都序列化了,这是不是浪费带宽啊,毕竟走服务器不如直接传字符串然后再从CDN上下载合适。确实有点让我经验了,同理肯定也有叫 Mirror.NetworkReaderExtensions不在过多提及

ReaderWriterProcessor

我们回到ProcessAssemblyClasses方法

 foreach (TypeDefinition klass in assembly.MainModule.Types)
 {
     // if assembly has any network message then it is modified
     modified |= LoadMessageReadWriter(CurrentAssembly.MainModule, writers, readers, klass, ref WeavingFailed);
 }

方式估计也差不多,之前有提到过Mirror有很多NetworkMessage,用于固定的生命周期事件消息,就是扫描继承自NetworkMessage的所有结构体,然后将对应的读写序列化方法放置到Readers,Writers中

Weaver

modified |= WeaveModule(moduleDefinition);

看名字查出来是干嘛的,看下源码

bool WeaveModule(ModuleDefinition moduleDefinition)
{
    bool modified = false;

    Stopwatch watch = Stopwatch.StartNew();
    watch.Start();

    // ModuleDefinition.Types only finds top level types.
    // GetAllTypes recursively finds all nested types as well.
    // fixes nested types not being weaved, for example:
    //     class Parent {              // ModuleDefinition.Types finds this
    //         class Child {           // .Types.NestedTypes finds this
    //             class GrandChild {} // only GetAllTypes finds this too
    //         }
    //     }
    // note this is not about inheritance, only about type definitions.
    // see test: NetworkBehaviourTests.DeeplyNested()
    foreach (TypeDefinition td in moduleDefinition.GetAllTypes())
    {
        if (td.IsClass && td.BaseType.CanBeResolved())
        {
            modified |= WeaveNetworkBehavior(td);
            modified |= ServerClientAttributeProcessor.Process(weaverTypes, Log, td, ref WeavingFailed);
        }
    }

    watch.Stop();
    Console.WriteLine($"Weave behaviours and messages took {watch.ElapsedMilliseconds} milliseconds");

    return modified;
}

哦,好吧,对当前的程序集遍历,然后处理NetworkBehavior及其派生类,注释中说了,为什么要使用GetAllTypes,核心需要关注的就是

WeaveNetworkBehavior ServerClientAttributeProcessor.Process `

WeaveNetworkBehavior

 bool WeaveNetworkBehavior(TypeDefinition td)
 {
     if (!td.IsClass)
         return false;

     if (!td.IsDerivedFrom<NetworkBehaviour>())
     {
         if (td.IsDerivedFrom<UnityEngine.MonoBehaviour>())
             MonoBehaviourProcessor.Process(Log, td, ref WeavingFailed);
         return false;
     }

     // process this and base classes from parent to child order

     List<TypeDefinition> behaviourClasses = new List<TypeDefinition>();

     TypeDefinition parent = td;
     while (parent != null)
     {
         if (parent.Is<NetworkBehaviour>())
         {
             break;
         }

         try
         {
             behaviourClasses.Insert(0, parent);
             parent = parent.BaseType.Resolve();
         }
         catch (AssemblyResolutionException)
         {
             // this can happen for plugins.
             //Console.WriteLine("AssemblyResolutionException: "+ ex.ToString());
             break;
         }
     }

     bool modified = false;
     foreach (TypeDefinition behaviour in behaviourClasses)
     {
         modified |= new NetworkBehaviourProcessor(CurrentAssembly, weaverTypes, syncVarAccessLists, writers, readers, Log, behaviour).Process(ref WeavingFailed);
     }
     return modified;
 }

如果当前Type是从NetworkBehaviour派生的,则往上找父类直到NetworkBehaviour为止,然后优先处理parent再处理子类的顺序进行,这里回直接新建一个NetworkBehaviourProcessor的程序进行处理,同时NetworkBehaviourProcessor内会判断Type是否已经被标记过,标记其被处理过的方式是,直接再当前的Type新增一个Weaved方法,如果有则表示被处理过了直接返回。

 public bool Process(ref bool WeavingFailed)
 {
     // only process once
     if (WasProcessed(netBehaviourSubclass))
     {
         return false;
     }

     MarkAsProcessed(netBehaviourSubclass);

     // deconstruct tuple and set fields
     (syncVars, syncVarNetIds, syncVarHookDelegates) = syncVarAttributeProcessor.ProcessSyncVars(netBehaviourSubclass, ref WeavingFailed);

     syncObjects = SyncObjectProcessor.FindSyncObjectsFields(writers, readers, Log, netBehaviourSubclass, ref WeavingFailed);

     ProcessMethods(ref WeavingFailed);
     if (WeavingFailed)
     {
         // originally Process returned true in every case, except if already processed.
         // maybe return false here in the future.
         return true;
     }

     // inject initializations into static & instance constructor
     InjectIntoStaticConstructor(ref WeavingFailed);
     InjectIntoInstanceConstructor(ref WeavingFailed);

     GenerateSerialization(ref WeavingFailed);
     if (WeavingFailed)
     {
         // originally Process returned true in every case, except if already processed.
         // maybe return false here in the future.
         return true;
     }

     GenerateDeSerialization(ref WeavingFailed);
     return true;
 }

SyncVarAttributeProcessor

ProcessSyncVars

syncVarAttributeProcessor.ProcessSyncVars针对syncVars进行处理,主要流程是遍历所有的FieldDefinition,判断字段上是否存在SyncVarAttribute并做规则检测比如必须是非静态修饰符,不得存在泛型,如果SyncObject(如SyncList)则不需要附件[SyncVar]。然后通过ProcessSyncVar方法,根据字段的类型生成对应的Setter Getter方法,类型判断有两个细节,如果是NetworkBehavior或者其派生类,则生成一个$“__{fd.Name}NetId"格式,类型为NetworkBehaviourSyncVar的字段,并将对应的Setter Getter逻辑定向到该字段上。NetworkBehaviourSyncVar自己持有NetId以及ComponentsIndex,如果是NetworkIdentity则新增 $”__{fd.Name}NetId",为行为uint的字段,也将Setter Getter的逻辑定向到该字段上,

 public void ProcessSyncVar(TypeDefinition td, FieldDefinition fd, Dictionary<FieldDefinition, FieldDefinition> syncVarNetIds, Dictionary<FieldDefinition, (FieldDefinition hookDelegateField, MethodDefinition hookMethod)> syncVarHookDelegates, long dirtyBit, ref bool WeavingFailed)
 {
     string originalName = fd.Name;

     // GameObject/NetworkIdentity SyncVars have a new field for netId
     FieldDefinition netIdField = null;
     // NetworkBehaviour has different field type than other NetworkIdentityFields
     // handle both NetworkBehaviour and inheritors.
     // fixes: https://github.com/MirrorNetworking/Mirror/issues/2939
     if (fd.FieldType.IsDerivedFrom<NetworkBehaviour>() || fd.FieldType.Is<NetworkBehaviour>())
     {
         netIdField = new FieldDefinition($"___{fd.Name}NetId",
            FieldAttributes.Family, // needs to be protected for generic classes, otherwise access isn't allowed
            weaverTypes.Import<NetworkBehaviourSyncVar>());
         netIdField.DeclaringType = td;

         syncVarNetIds[fd] = netIdField;
     }
     else if (fd.FieldType.IsNetworkIdentityField())
     {
         netIdField = new FieldDefinition($"___{fd.Name}NetId",
             FieldAttributes.Family, // needs to be protected for generic classes, otherwise access isn't allowed
             weaverTypes.Import<uint>());
         netIdField.DeclaringType = td;

         syncVarNetIds[fd] = netIdField;
     }

     MethodDefinition get = GenerateSyncVarGetter(fd, originalName, netIdField);
     MethodDefinition set = GenerateSyncVarSetter(td, fd, originalName, dirtyBit, netIdField, syncVarHookDelegates, ref WeavingFailed);

     //NOTE: is property even needed? Could just use a setter function?
     //create the property
     PropertyDefinition propertyDefinition = new PropertyDefinition($"Network{originalName}", PropertyAttributes.None, fd.FieldType)
     {
         GetMethod = get,
         SetMethod = set
     };

     //add the methods and property to the type.
     td.Methods.Add(get);
     td.Methods.Add(set);
     td.Properties.Add(propertyDefinition);
     syncVarAccessLists.replacementSetterProperties[fd] = set;

     // replace getter field if GameObject/NetworkIdentity so it uses
     // netId instead
     // -> only for GameObjects, otherwise an int syncvar's getter would
     //    end up in recursion.
     if (fd.FieldType.IsNetworkIdentityField())
     {
         syncVarAccessLists.replacementGetterProperties[fd] = get;
     }
 }

代理Getter,Setter的生成逻辑在 GenerateSyncVarGetter,GenerateSyncVarSetter,原理大概类似,只选择其中一个进行了解

GenerateSyncVarGetter

public MethodDefinition GenerateSyncVarGetter(FieldDefinition fd, string originalName, FieldDefinition netFieldId)
{
    //Create the get method
    MethodDefinition get = new MethodDefinition(
        $"get_Network{originalName}", MethodAttributes.Public |
                                      MethodAttributes.SpecialName |
                                      MethodAttributes.HideBySig,
            fd.FieldType);

    ILProcessor worker = get.Body.GetILProcessor();

    FieldReference fr;
    if (fd.DeclaringType.HasGenericParameters)
    {
        fr = fd.MakeHostInstanceGeneric();
    }
    else
    {
        fr = fd;
    }

    FieldReference netIdFieldReference = null;
    if (netFieldId != null)
    {
        if (netFieldId.DeclaringType.HasGenericParameters)
        {
            netIdFieldReference = netFieldId.MakeHostInstanceGeneric();
        }
        else
        {
            netIdFieldReference = netFieldId;
        }
    }

    // [SyncVar] GameObject?
    if (fd.FieldType.Is<UnityEngine.GameObject>())
    {
        // return this.GetSyncVarGameObject(ref field, uint netId);
        // this.
        worker.Emit(OpCodes.Ldarg_0);
        worker.Emit(OpCodes.Ldarg_0);
        worker.Emit(OpCodes.Ldfld, netIdFieldReference);
        worker.Emit(OpCodes.Ldarg_0);
        worker.Emit(OpCodes.Ldflda, fr);
        worker.Emit(OpCodes.Call, weaverTypes.getSyncVarGameObjectReference);
        worker.Emit(OpCodes.Ret);
    }
    // [SyncVar] NetworkIdentity?
    else if (fd.FieldType.Is<NetworkIdentity>())
    {
        // return this.GetSyncVarNetworkIdentity(ref field, uint netId);
        // this.
        worker.Emit(OpCodes.Ldarg_0);
        worker.Emit(OpCodes.Ldarg_0);
        worker.Emit(OpCodes.Ldfld, netIdFieldReference);
        worker.Emit(OpCodes.Ldarg_0);
        worker.Emit(OpCodes.Ldflda, fr);
        worker.Emit(OpCodes.Call, weaverTypes.getSyncVarNetworkIdentityReference);
        worker.Emit(OpCodes.Ret);
    }
    // handle both NetworkBehaviour and inheritors.
    // fixes: https://github.com/MirrorNetworking/Mirror/issues/2939
    else if (fd.FieldType.IsDerivedFrom<NetworkBehaviour>() || fd.FieldType.Is<NetworkBehaviour>())
    {
        // return this.GetSyncVarNetworkBehaviour<T>(ref field, uint netId);
        // this.
        worker.Emit(OpCodes.Ldarg_0);
        worker.Emit(OpCodes.Ldarg_0);
        worker.Emit(OpCodes.Ldfld, netIdFieldReference);
        worker.Emit(OpCodes.Ldarg_0);
        worker.Emit(OpCodes.Ldflda, fr);
        MethodReference getFunc = weaverTypes.getSyncVarNetworkBehaviourReference.MakeGeneric(assembly.MainModule, fd.FieldType);
        worker.Emit(OpCodes.Call, getFunc);
        worker.Emit(OpCodes.Ret);
    }
    // [SyncVar] int, string, etc.
    else
    {
        worker.Emit(OpCodes.Ldarg_0);
        worker.Emit(OpCodes.Ldfld, fr);
        worker.Emit(OpCodes.Ret);
    }

    get.Body.Variables.Add(new VariableDefinition(fd.FieldType));
    get.Body.InitLocals = true;
    get.SemanticsAttributes = MethodSemanticsAttributes.Getter;

    return get;
}

新增的Getter代理,名字格式为$“get_Network{originalName}”,然后对fd的类型分别进行处理

类型处理方式
基础类型直接返回
NetworkIdentitygetSyncVarNetworkIdentityReference
NetworkBehaviour及其派生子类getSyncVarNetworkBehaviourReference(MakeGeneric填写调用函数时的泛型)
GameObjectgetSyncVarGameObjectReference

getSyncVarNetworkIdentityReference,getSyncVarNetworkBehaviourReference,getSyncVarGameObjectReference

三个方法的定义都存在于NetworkBehaviour

  • getSyncVarGameObjectReference依靠的还是NetId,在Setter方法中取出identity然后Getter的时候通过netId在NetworkClient.spawned查找,然后拿到gameobject,所以能进行Sync的GameObject一定是有Identity的才可以
  • getSyncVarNetworkBehaviourReference 依靠新增字段类型保存的NetworkBehaviourSyncVar netId和ComponentIndex来定位
  • getSyncVarNetworkIdentityReference 依靠新增字段uint保存的netId来定位

以下时相关的代码具体实现

GetSyncVarNetworkBehaviour

GetSyncVarNetworkIdentity

GetSyncVarGameObject

protected T GetSyncVarNetworkBehaviour<T>(NetworkBehaviourSyncVar syncNetBehaviour, ref T behaviourField) where T : NetworkBehaviour
{
    // server always uses the field
    // if neither, fallback to original field
    // fixes: https://github.com/MirrorNetworking/Mirror/issues/3447
    if (isServer || !isClient)
    {
        return behaviourField;
    }

    // client always looks up based on netId because objects might get in and out of range
    // over and over again, which shouldn't null them forever
    if (!NetworkClient.spawned.TryGetValue(syncNetBehaviour.netId, out NetworkIdentity identity))
    {
        return null;
    }

    behaviourField = identity.NetworkBehaviours[syncNetBehaviour.componentIndex] as T;
    return behaviourField;
}

protected NetworkIdentity GetSyncVarNetworkIdentity(uint netId, ref NetworkIdentity identityField)
{
    // server always uses the field
    // if neither, fallback to original field
    // fixes: https://github.com/MirrorNetworking/Mirror/issues/3447
    if (isServer || !isClient)
    {
        return identityField;
    }

    // client always looks up based on netId because objects might get in and out of range
    // over and over again, which shouldn't null them forever
    NetworkClient.spawned.TryGetValue(netId, out identityField);
    return identityField;
}
 protected GameObject GetSyncVarGameObject(uint netId, ref GameObject gameObjectField)
 {
     // server always uses the field
     // if neither, fallback to original field
     // fixes: https://github.com/MirrorNetworking/Mirror/issues/3447
     if (isServer || !isClient)
     {
         return gameObjectField;
     }

     // client always looks up based on netId because objects might get in and out of range
     // over and over again, which shouldn't null them forever
     if (NetworkClient.spawned.TryGetValue(netId, out NetworkIdentity identity) && identity != null)
         return gameObjectField = identity.gameObject;
     return null;
 }

将生成的Setter Getter Method 放置到syncVarAccessLists的replacementSetterProperties和replacementGetterProperties,ProcessSyncVar就算结束了,另外这里生成了一个Properly名称格式为$"Network{fd.Name}"放到了当前的Type中,对了GenerateSyncVarSetter中还生成了[SynVar(hook=)]的hook钩子,利用的Action。

syncObjects = SyncObjectProcessor.FindSyncObjectsFields(writers, readers, Log, netBehaviourSubclass, ref WeavingFailed);是为了处理SyncObject,他的派生类有SyncDictionary,SyncHashSet,SyncDicitonary,SyncList,SyncSet,SyncSortedSet,逻辑基本差不多,SyncObject有自己的序列化反序列化接口,允许自行实现对数据的差量全量更新。

ProcessMethods

ProcessMethods(ref WeavingFailed);正式开始对ClientRPC,Command,TargetRPC注解方法进行处理

void ProcessMethods(ref bool WeavingFailed)
{
    HashSet<string> names = new HashSet<string>();

    // copy the list of methods because we will be adding methods in the loop
    List<MethodDefinition> methods = new List<MethodDefinition>(netBehaviourSubclass.Methods);
    // find command and RPC functions
    foreach (MethodDefinition md in methods)
    {
        foreach (CustomAttribute ca in md.CustomAttributes)
        {
            if (ca.AttributeType.Is<CommandAttribute>())
            {
                ProcessCommand(names, md, ca, ref WeavingFailed);
                break;
            }

            if (ca.AttributeType.Is<TargetRpcAttribute>())
            {
                ProcessTargetRpc(names, md, ca, ref WeavingFailed);
                break;
            }

            if (ca.AttributeType.Is<ClientRpcAttribute>())
            {
                ProcessClientRpc(names, md, ca, ref WeavingFailed);
                break;
            }
        }
    }
}

CommandProcessor.csd

ProcessCommandCall

 public static MethodDefinition ProcessCommandCall(WeaverTypes weaverTypes, Writers writers, Logger Log, TypeDefinition td, MethodDefinition md, CustomAttribute commandAttr, ref bool WeavingFailed)
 {
     MethodDefinition cmd = MethodProcessor.SubstituteMethod(Log, td, md, ref WeavingFailed);

     ILProcessor worker = md.Body.GetILProcessor();

     NetworkBehaviourProcessor.WriteSetupLocals(worker, weaverTypes);

     // NetworkWriter writer = new NetworkWriter();
     NetworkBehaviourProcessor.WriteGetWriter(worker, weaverTypes);

     // write all the arguments that the user passed to the Cmd call
     if (!NetworkBehaviourProcessor.WriteArguments(worker, writers, Log, md, RemoteCallType.Command, ref WeavingFailed))
         return null;

     int channel = commandAttr.GetField("channel", 0);
     bool requiresAuthority = commandAttr.GetField("requiresAuthority", true);

     // invoke internal send and return
     // load 'base.' to call the SendCommand function with
     worker.Emit(OpCodes.Ldarg_0);
     // pass full function name to avoid ClassA.Func <-> ClassB.Func collisions
     worker.Emit(OpCodes.Ldstr, md.FullName);
     // pass the function hash so we don't have to compute it at runtime
     // otherwise each GetStableHash call requires O(N) complexity.
     // noticeable for long function names:
     // https://github.com/MirrorNetworking/Mirror/issues/3375
     worker.Emit(OpCodes.Ldc_I4, md.FullName.GetStableHashCode());
     // writer
     worker.Emit(OpCodes.Ldloc_0);
     worker.Emit(OpCodes.Ldc_I4, channel);
     // requiresAuthority ? 1 : 0
     worker.Emit(requiresAuthority ? OpCodes.Ldc_I4_1 : OpCodes.Ldc_I4_0);
     worker.Emit(OpCodes.Call, weaverTypes.sendCommandInternal);

     NetworkBehaviourProcessor.WriteReturnWriter(worker, weaverTypes);

     worker.Emit(OpCodes.Ret);
     return cmd;
 }

MethodProcessor

SubstituteMethod

以固定格式命名然后copy目标method元数据到新的方法中,这一步已经将新增的方法添加到了td中,即类中,且执行之后md的Body是空的,灭有任何代码逻辑

// For a function like
//   [ClientRpc] void RpcTest(int value),
// Weaver substitutes the method and moves the code to a new method:
//   UserCode_RpcTest(int value) <- contains original code
//   RpcTest(int value) <- serializes parameters, sends the message
//
// Note that all the calls to the method remain untouched.
// FixRemoteCallToBaseMethod replaces them afterwards.
public static MethodDefinition SubstituteMethod(Logger Log, TypeDefinition td, MethodDefinition md, ref bool WeavingFailed)
{
    string newName = Weaver.GenerateMethodName(RpcPrefix, md);

    MethodDefinition cmd = new MethodDefinition(newName, md.Attributes, md.ReturnType);

    // force the substitute method to be protected.
    // -> public would show in the Inspector for UnityEvents as
    //    User_CmdUsePotion() etc. but the user shouldn't use those.
    // -> private would not allow inheriting classes to call it, see
    //    OverrideVirtualWithBaseCallsBothVirtualAndBase test.
    // -> IL has no concept of 'protected', it's called IsFamily there.
    cmd.IsPublic = false;
    cmd.IsFamily = true;

    // add parameters
    foreach (ParameterDefinition pd in md.Parameters)
    {
        cmd.Parameters.Add(new ParameterDefinition(pd.Name, ParameterAttributes.None, pd.ParameterType));
    }

    // swap bodies
    (cmd.Body, md.Body) = (md.Body, cmd.Body);

    // Move over all the debugging information
    foreach (SequencePoint sequencePoint in md.DebugInformation.SequencePoints)
        cmd.DebugInformation.SequencePoints.Add(sequencePoint);
    md.DebugInformation.SequencePoints.Clear();

    foreach (CustomDebugInformation customInfo in md.CustomDebugInformations)
        cmd.CustomDebugInformations.Add(customInfo);
    md.CustomDebugInformations.Clear();

    (md.DebugInformation.Scope, cmd.DebugInformation.Scope) = (cmd.DebugInformation.Scope, md.DebugInformation.Scope);

    td.Methods.Add(cmd);

    FixRemoteCallToBaseMethod(Log, td, cmd, ref WeavingFailed);
    return cmd;
}

Weaver.cs

GenerateMethodName

附带前缀的方法名称生成,使用__作为间隔符号,附加一个initialPrefix前缀,不做过多说明

// remote actions now support overloads,
// -> but IL2CPP doesnt like it when two generated methods
// -> have the same signature,
// -> so, append the signature to the generated method name,
// -> to create a unique name
// Example:
// RpcTeleport(Vector3 position) -> InvokeUserCode_RpcTeleport__Vector3()
// RpcTeleport(Vector3 position, Quaternion rotation) -> InvokeUserCode_RpcTeleport__Vector3Quaternion()
// fixes https://github.com/vis2k/Mirror/issues/3060
public static string GenerateMethodName(string initialPrefix, MethodDefinition md)
{
    initialPrefix += md.Name;

    for (int i = 0; i < md.Parameters.Count; ++i)
    {
        // with __ so it's more obvious that this is the parameter suffix.
        // otherwise RpcTest(int) => RpcTestInt(int) which is not obvious.
        initialPrefix += $"__{md.Parameters[i].ParameterType.Name}";
    }

    return initialPrefix;
}

FixRemoteCallToBaseMethod

注释说的也很清楚,一句话替换递归回调

// For a function like
//   [ClientRpc] void RpcTest(int value),
// Weaver substitutes the method and moves the code to a new method:
//   UserCode_RpcTest(int value) <- contains original code
//   RpcTest(int value) <- serializes parameters, sends the message
//
// FixRemoteCallToBaseMethod replaces all calls to
//   RpcTest(value)
// with
//   UserCode_RpcTest(value)
public static void FixRemoteCallToBaseMethod(Logger Log, TypeDefinition type, MethodDefinition method, ref bool WeavingFailed)
{
    string callName = method.Name;

    // Cmd/rpc start with Weaver.RpcPrefix
    // e.g. CallCmdDoSomething
    if (!callName.StartsWith(RpcPrefix))
        return;

    // e.g. CmdDoSomething
    string baseRemoteCallName = method.Name.Substring(RpcPrefix.Length);

    foreach (Instruction instruction in method.Body.Instructions)
    {
        // is this instruction a Call to a method?
        // if yes, output the method so we can check it.
        if (IsCallToMethod(instruction, out MethodDefinition calledMethod))
        {
            // when considering if 'calledMethod' is a call to 'method',
            // we originally compared .Name.
            //
            // to fix IL2CPP build bugs with overloaded Rpcs, we need to
            // generated rpc names like
            //   RpcTest(string value) => RpcTestString(strig value)
            //   RpcTest(int value)    => RpcTestInt(int value)
            // to make them unique.
            //
            // calledMethod.Name is still "RpcTest", so we need to
            // convert this to the generated name as well before comparing.
            string calledMethodName_Generated = Weaver.GenerateMethodName("", calledMethod);
            if (calledMethodName_Generated == baseRemoteCallName)
            {
                TypeDefinition baseType = type.BaseType.Resolve();
                MethodDefinition baseMethod = baseType.GetMethodInBaseType(callName);

                if (baseMethod == null)
                {
                    Log.Error($"Could not find base method for {callName}", method);
                    WeavingFailed = true;
                    return;
                }

                if (!baseMethod.IsVirtual)
                {
                    Log.Error($"Could not find base method that was virtual {callName}", method);
                    WeavingFailed = true;
                    return;
                }

                instruction.Operand = baseMethod;
            }
        }
    }
}

ProcessCommandCall

    // invoke internal send and return
     // load 'base.' to call the SendCommand function with
     worker.Emit(OpCodes.Ldarg_0);
     // pass full function name to avoid ClassA.Func <-> ClassB.Func collisions
     worker.Emit(OpCodes.Ldstr, md.FullName);
     // pass the function hash so we don't have to compute it at runtime
     // otherwise each GetStableHash call requires O(N) complexity.
     // noticeable for long function names:
     // https://github.com/MirrorNetworking/Mirror/issues/3375
     worker.Emit(OpCodes.Ldc_I4, md.FullName.GetStableHashCode());
     // writer
     worker.Emit(OpCodes.Ldloc_0);
     worker.Emit(OpCodes.Ldc_I4, channel);
     // requiresAuthority ? 1 : 0
     worker.Emit(requiresAuthority ? OpCodes.Ldc_I4_1 : OpCodes.Ldc_I4_0);
     worker.Emit(OpCodes.Call, weaverTypes.sendCommandInternal);

     NetworkBehaviourProcessor.WriteReturnWriter(worker, weaverTypes);

     worker.Emit(OpCodes.Ret);

这里的worker即md的方法体,请注意在代码执行之前方法体中指令是空的,先前有讲到Mirror动态生成了方法并将原方法方法体进行了替换,这里对这段代码进行解释说明不想关注OPCode的话,可以理解成
base.SendCommandInternal(md.FullName,md.FullName.GetStableHashCode(),writer,channel, requiresAuthority)
OpCodes.Ldarg_0 将方法的第一个参数放置到栈顶,对于非静态方法,第一个参数通常为隐式函数this,OpCodes.Ldstr 将字符串压入栈顶,OPCodes.Ldc_I4 压入4字节hashCode到栈顶,压入局部变量0位置插槽的引用(这里特指的NetworkWriter,因为在前面的代码中已经对Writer进行了初始化, NetworkBehaviourProcessor.WriteGetWriter(worker, weaverTypes);)压入常量4bitchannel的值到栈顶,压入4bit的0或者1到栈顶,bool也占据四字节。OpCodes这个指令后面跟随的式一个MethodReference,就是一个方法引用,我们脑部下虚拟执行的时候会发生上面,通过Call指令拿到下一个4个字节,这四个字节式方法的内存地址,在内存地址,会记录函数的逻辑代码位置,以及参数的个数,虚拟机此时应该是从新开启一个新的栈帧,将参数弹出放置到对应内存位置上,然后从从该方法的逻辑代码地址处开始执行新的函数逻辑(猜测可能有误,欢迎指正)

未完待续… 这完全脱离当初想要,面向新手了解Mirror的初衷了,一定的要控制才行了

作为虚拟现实和增强现实领域的主要引擎之一,Unity Mirror 是一种基于网络的多人游戏解决方案,使游戏开发者能够轻松地创建多人游戏。在本教程中,我们将介绍如何使用 Unity Mirror 创建一个简单的多人游戏。 1. 安装 Unity Mirror 首先,我们需要在 Unity 中安装 Unity Mirror。您可以在 Unity Asset Store 中找到 Unity Mirror 并下载。下载完成后,打开 Unity 并将 Unity Mirror 导入到您的项目中。 2. 创建网络管理器 接下来,我们需要创建一个网络管理器。在 Unity 中,右键单击场景并选择“Create Empty”以创建一个新对象。将其重命名为“NetworkManager”。 3. 添加网络组件 选择“NetworkManager”对象并打开“Inspector”面板。在面板上,单击“Add Component”并搜索“NetworkManager”。选择“NetworkManager”组件并将其添加到对象上。 4. 配置网络组件 在“Network Manager”组件的“Inspector”面板中,您可以配置网络游戏的基本设置。例如,您可以选择是创建一个主机还是加入一个现有游戏。您还可以设置最大玩家数量、游戏端口和其他网络设置。 5. 创建玩家对象 现在,我们需要创建一个玩家对象。右键单击场景并选择“Create Empty”以创建一个新对象。将其重命名为“Player”。 6. 添加网络身份验证器 选择“Player”对象并打开“Inspector”面板。单击“Add Component”并搜索“NetworkIdentity”。选择“NetworkIdentity”组件并将其添加到对象上。 7. 配置网络身份验证器 在“NetworkIdentity”组件的“Inspector”面板中,您可以配置网络身份验证器的设置。例如,您可以选择此对象是否应在网络中可见,并设置其唯一标识符。 8. 添加玩家控制器 选择“Player”对象并打开“Inspector”面板。单击“Add Component”并搜索“PlayerController”。选择“PlayerController”组件并将其添加到对象上。 9. 编写脚本 最后,我们需要编写一些脚本来控制玩家对象。我们需要一个脚本来处理用户输入,并将其发送到服务器。我们还需要一个脚本来处理服务器上的用户输入,并将其发送到所有连接的客户端。 10. 测试多人游戏 现在,您可以测试您的多人游戏。在 Unity 中,单击“Play”以启动游戏。您可以使用多个实例的 Unity 来模拟多个玩家。在游戏中,您可以控制您的玩家对象,并与其他玩家进行互动。 总结 Unity Mirror 是一种强大的多人游戏解决方案,使游戏开发者能够轻松地创建多人游戏。在本教程中,我们介绍了如何使用 Unity Mirror 创建一个简单的多人游戏。通过这些步骤,您可以开始探索 Unity Mirror 的功能,并创建自己的多人游戏。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值