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的初衷了,一定的要控制才行了

Unity3D是一款非常流行的游戏引擎,它广泛应用于游戏开发、虚拟现实、增强现实等领域。对于初学者来说,学习Unity3D需要了解其基本概念、操作技巧和流程。为了从入门到精通,需要掌握以下个阶段: 第一阶段是入门阶段。这个阶段的关键是掌握Unity3D的基本概念和操作方法。首先,需要了解Unity3D的界面结构和菜单命令,熟悉不同面板的功能,包括场景窗口、资源窗口、控制台、层次结构等。其次,需要掌握Unity3D中的对象、组件和预制件的概念。通过学习Object和Component的类结构,了解它们的特性和作用,明白游戏对象的层级关系、物理模拟和碰撞检测等原理。最后,需要掌握脚本编写的基础语法,理解脚本与其他组件的交互方式和生命周期。 第二阶段是中级阶段。在入门阶段的基础上,需要进一步深入了解Unity3D引擎的高级技术和实际应用。主要包括:游戏设计模式、界面布局和UI设计、动画控制和剪辑编辑、材质和着色器的使用、粒子系统和特效处理等。此外,需要了解Unity3D的性能优化和调试方法,运用Profiler、Frame Debugger等工具分析游戏引擎的内部运行机制,减少游戏卡顿和崩溃的情况。 第阶段是精通阶段。这个阶段的关键是掌握Unity3D引擎的高级特性和复杂游戏的开发流程。主要涉及:脚本优化和高级算法的实现、网络游戏开发和多人游戏场景同步、人工智能和路径规划等。此外,需要了解Unity3D的插件开发和资产管理、持续集成和版本控制、移动平台和AR/VR领域的开发规范等。最终,达到像Unity3D官方开发者一样的能力水平,能够独立完成复杂游戏的开发和运营,或参与到较大规模的团队开发项目中,成为一名优秀的Unity3D开发者。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值