Unity Mirror 从入门到入神(三)

文章目录

  • Unity Mirror 从入门到入神(三)
    • NetworkManagerMode
      • StartHost
      • SetupServer
    • NetworkServer
      • Listen
      • RegisterMessageHandlers
      • OnCommandMessage
    • RemoteCall.cs
      • CommandRequiresAuthority
    • NetworkIdentity
      • HandleRemoteCall
    • RemoteCall.cs
      • Invoke
      • RegisterCommand RegisterRpc
      • RegisterDelegate
    • ILPostProcessorHook
      • WillProcess
      • Process
    • InMemoryAssembly
    • ICompiledAssembly
    • TypeReference
    • MethodReference
    • ParameterDefinition
    • ILProcessor
    • Weaver
      • weaver
      • WeaverTypes
    • ReaderWriterProcessor
      • Process
      • ProcessAssemblyClasses
      • LoadDeclaredWriters
      • LoadDeclaredReaders
    • NetworkWriterExtensions
      • WriteGameObject
      • WriteNetworkIdentity
      • WriteNetworkBehaviour
      • WriteSprite
      • WriteTexture2D
    • ReaderWriterProcessor
    • Weaver
      • WeaveNetworkBehavior
    • SyncVarAttributeProcessor
      • ProcessSyncVars
      • GenerateSyncVarGetter
      • GetSyncVarNetworkBehaviour
      • GetSyncVarNetworkIdentity
      • GetSyncVarGameObject
      • ProcessMethods
    • CommandProcessor.csd
      • ProcessCommandCall
    • MethodProcessor
      • SubstituteMethod
    • Weaver.cs
      • GenerateMethodName
      • FixRemoteCallToBaseMethod
      • ProcessCommandCall

前序文章

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 firstSetupServer();// 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 directlyelse{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 connectionsNetworkServer.Listen(maxConnections); //启动监听开启网络,并初始化监听方法// this must be after Listen(), since that registers the default message handlersRegisterServerMessages(); // 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 listenif (!dontListen){Transport.active.ServerStart();if (Transport.active is PortTransport portTransport){if (Utils.IsHeadless()){
#if !UNITY_EDITORConsole.ForegroundColor = ConsoleColor.Green;Console.WriteLine($"Server listening on port {portTransport.Port}");Console.ResetColor();
#elseDebug.Log($"Server listening on port {portTransport.Port}");
#endif}}elseDebug.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 clientAuthorityOwnerbool 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 destroyedif (this == null){Debug.LogWarning($"{remoteCallType} [{functionHash}] received for deleted object [netId={netId}]");return;}// find the right component to invoke the function onif (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 componentinvoker.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.RpcUseushort 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 itpublic const string IgnoreDefine = "ILPP_IGNORE";

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

WillProcess

    // from CompilationFinishedHookconst 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 itpublic const string IgnoreDefine = "ILPP_IGNORE";// check if assembly has the 'ignore' definestatic 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 MemoryStreambyte[] 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 modifiedif (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 hereif (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 setupif (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 CreateGeneratedCodeClassCreateGeneratedCodeClass();// WeaverList depends on WeaverTypes setup because it uses ImportsyncVarAccessLists = 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 NetworkMessagesmodified = 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 functionsmoduleDefinition.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.dllif (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 ImportsyncVarAccessLists = 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 testsProcessMirrorAssemblyClasses(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 abstractif (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 modifiedmodified |= 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 signaturebool 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 signaturebool 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 hashint 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 NetworkIdentityif (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 enoughwriter.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 modifiedmodified |= 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 orderList<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 onceif (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 constructorInjectIntoStaticConstructor(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 netIdFieldDefinition netIdField = null;// NetworkBehaviour has different field type than other NetworkIdentityFields// handle both NetworkBehaviour and inheritors.// fixes: https://github.com/MirrorNetworking/Mirror/issues/2939if (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 allowedweaverTypes.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 allowedweaverTypes.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 propertyPropertyDefinition 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 methodMethodDefinition 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/2939else 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/3447if (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 foreverif (!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/3447if (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 foreverNetworkClient.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/3447if (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 foreverif (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 loopList<MethodDefinition> methods = new List<MethodDefinition>(netBehaviourSubclass.Methods);// find command and RPC functionsforeach (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 callif (!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 withworker.Emit(OpCodes.Ldarg_0);// pass full function name to avoid ClassA.Func <-> ClassB.Func collisionsworker.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/3375worker.Emit(OpCodes.Ldc_I4, md.FullName.GetStableHashCode());// writerworker.Emit(OpCodes.Ldloc_0);worker.Emit(OpCodes.Ldc_I4, channel);// requiresAuthority ? 1 : 0worker.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 parametersforeach (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 informationforeach (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. CallCmdDoSomethingif (!callName.StartsWith(RpcPrefix))return;// e.g. CmdDoSomethingstring 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 withworker.Emit(OpCodes.Ldarg_0);// pass full function name to avoid ClassA.Func <-> ClassB.Func collisionsworker.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/3375worker.Emit(OpCodes.Ldc_I4, md.FullName.GetStableHashCode());// writerworker.Emit(OpCodes.Ldloc_0);worker.Emit(OpCodes.Ldc_I4, channel);// requiresAuthority ? 1 : 0worker.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的初衷了,一定的要控制才行了

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/pingmian/14854.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【问题处理】maven一直提示artemis-http-client-1.1.8.jar报错(2024-05-25)

项目使用了视频监控&#xff0c;里面涉及到海康威视的视频监控。 问题&#xff1a; pom在导入maven时&#xff0c;报错“Could not find artifact com.artemis:http-client:jar:1.1.8 ” 原因&#xff1a; 根据平台提供的maven地址&#xff0c;填写进pom文件中&#xff0c;编…

汇编-16位汇编环境搭建

16位汇编环境 在学习16位汇编时&#xff0c;我选择的环境是在VMware中安装Windows XP虚拟机来学习&#xff1b;因为Windows XP提供了兼容的DOS环境&#xff0c;可以直接运行和调试16位汇编程序&#xff1b;在win10&#xff0c;win11环境中原生不支持直接运行 16 位程序&#x…

房地产画册制作成手机在线翻页效果

​随着科技的飞速发展&#xff0c;移动互联网已经深入到人们的日常生活中。在这个数字化的时代&#xff0c;房地产行业也紧跟潮流&#xff0c;将画册制作成手机在线翻页效果&#xff0c;以满足消费者的阅读习惯。 房地产画册制作成手机在线翻页效果&#xff0c;不仅能够满足消费…

抖音商品API接口:开启电商自动化和数据洞察之门

在数字化转型的浪潮中&#xff0c;电商平台如抖音正通过其商品API接口&#xff0c;为商家和开发者提供了深入理解和利用电商数据的新途径。本篇文章将引导读者了解抖音商品API接口的基本概念、使用方法&#xff0c;并提供一个简单的代码示例&#xff0c;以教育和启发对电商自动…

mac清理软件推荐免费 mac清理系统数据怎么清理 cleanmymac和腾讯柠檬哪个好

macbook是苹果公司的一款高性能的笔记本电脑&#xff0c;受到了很多用户的喜爱。但是&#xff0c;随着使用时间的增长&#xff0c;macbook的系统也会积累一些垃圾文件&#xff0c;影响其运行速度和空间。那么&#xff0c;macbook系统清理软件推荐有哪些呢&#xff1f;macbook用…

263 基于matlab得到的频分复用(FDM,Frequency Division Multiplexing)实现

基于matlab得到的频分复用(FDM&#xff0c;Frequency Division Multiplexing)实现&#xff0c;仿真时录入三路声音信号进行处理&#xff0c;将用于传输信道的总带宽划分成三个子频带&#xff0c;经过复用以后再将录入的声音信号恢复出来。程序已调通&#xff0c;可直接运行。 2…

Docker | 基础指令

环境&#xff1a;centos8 参考&#xff1a; 安装 Docker | Docker 从入门到实践https://vuepress.mirror.docker-practice.com/install/ 安装Docker 卸载旧版本&#xff0c;安装依赖包&#xff0c;添加yum软件源&#xff0c;更新 yum 软件源缓存&#xff0c;安装 docker-ce…

AI助力农田作物智能化激光除草,基于轻量级YOLOv8n开发构建农田作物场景下常见20种杂草检测识别分析系统

随着科技的飞速发展&#xff0c;人工智能&#xff08;AI&#xff09;技术在各个领域的应用愈发广泛&#xff0c;其中农业领域也不例外。近年来&#xff0c;AI助力农田作物场景下智能激光除草的技术成为了农业领域的一大亮点&#xff0c;它代表着农业智能化、自动化的新趋势。智…

OAuth2.0

OAuth2.0 OAuth2.0是一种授权框架&#xff0c;用于授权第三方应用访问用户资源的方式。它允许用户将自己的信息&#xff08;如照片、视频等&#xff09;存储在一个服务提供商中&#xff0c;然后授权第三方应用访问这些信息&#xff0c;而无需提供用户名和密码给第三方应用。OAu…

基于地理坐标的高阶几何编辑工具算法(1)——目录

文章目录 背景目录效果相交面裁剪相离面吸附线分割面合并相交面合并相离面矩形绘制整形面 背景 在实际的地图编辑平台中&#xff0c;有一些场景是需要对几何面做修形操作&#xff0c;低效的做法是通过新增形点拖拽来实现。为了提高面几何的编辑效率&#xff0c;需要提供一些便…

公安数据库身份核验、C++便于集成的身份证实名认证接口开发文档

在当今数字化时代&#xff0c;信息安全与身份认证的精确性是企业发展的生命线。面对复杂多变的网络环境&#xff0c;如何高效、安全地完成用户身份核验&#xff0c;成为了每个企业的必修课。翔云提供了公安数据库身份核验接口&#xff0c;专为有需要的企业实现实名认证功能的无…

Java开发大厂面试第23讲:说一下 JVM 的内存布局和运行原理?

JVM&#xff08;Java Virtual Machine&#xff0c;Java 虚拟机&#xff09;顾名思义就是用来执行 Java 程序的“虚拟主机”&#xff0c;实际的工作是将编译的 class 代码&#xff08;字节码&#xff09;翻译成底层操作系统可以运行的机器码并且进行调用执行&#xff0c;这也是 …

虹科案例丨VLAN不再难懂:一台转换器+交换机轻松解锁VLAN配置

来源&#xff1a;虹科汽车电子 虹科案例丨VLAN不再难懂&#xff1a;一台转换器交换机轻松解锁VLAN配置 原文链接&#xff1a;https://mp.weixin.qq.com/s/5cFLWniozlppQGD7RcvgxA 欢迎关注虹科&#xff0c;为您提供最新资讯&#xff01; #VLAN #转换器 #交换机 导读 还在为…

顺序表-线性表的顺序表示

线性表 顺序表-线性表的顺序表示 #include <stdio.h>// 顺序表的大小 #define MaxSize 50 // 数据类型 typedef int ElemType; // 静态分配 // 此处可以不用给结构体起名称 使用时直接用别名即可 typedef struct {// 数组ElemType data[MaxSize];// 当前顺序表中元素个…

【Numpy】深入解析numpy中的ravel方法

NumPy中的ravel方法&#xff1a;一维化数组的艺术 &#x1f308; 欢迎莅临我的个人主页&#x1f448;这里是我深耕Python编程、机器学习和自然语言处理&#xff08;NLP&#xff09;领域&#xff0c;并乐于分享知识与经验的小天地&#xff01;&#x1f387; &#x1f393; 博主简…

实现复杂树结构返回(不含子树), 并且结点间建立关联

&#x1f4a1; 一句话结&#xff1a; 实现传感器和深度及采集的数值动态对应&#xff0c;将不规则的数据转变成固定列头的一行行数据。 &#x1f511; 关键信息点&#xff1a; 通过传感器编号和深度将传感器对应的数值与时间建立关联。使用SpringBootMyBatis框架实现动态查询…

RSA算法加解密

RSA算法的加密过程如下&#xff1a; 选择两个大素数①p和q&#xff0c;计算它们的乘积np*q计算欧拉函数φ(n)(p-1)*(q-1)选择一个整数e&#xff0c;满足1<e<φ(n)&#xff0c;且e与φ(n)互质计算e关于φ(n)的模逆元d&#xff0c;即满足e*d mod φ(n) 1的整数d②公钥为(…

【设计模式深度剖析】【2】【结构型】【装饰器模式】| 以去咖啡馆买咖啡为例 | 以穿衣服出门类比

&#x1f448;️上一篇:代理模式 目 录 装饰器模式定义英文原话直译如何理解呢&#xff1f;4个角色类图1. 抽象构件&#xff08;Component&#xff09;角色2. 具体构件&#xff08;Concrete Component&#xff09;角色3. 装饰&#xff08;Decorator&#xff09;角色4. 具体装饰…

2024电工杯数学建模A题Matlab代码+结果表数据教学

2024电工杯A题保姆级分析完整思路代码数据教学 A题题目&#xff1a;园区微电网风光储协调优化配置 以下仅展示部分&#xff0c;完整版看文末的文章 %A_1_1_A % 清除工作区 clear;clc;close all;warning off; %读取参数%正常读取 % P_LOADxlsread(附件1&#xff1a;各园区典…

前端 CSS 经典:SVG 描边动画

1. 原理 使用 css 中的 stroke 属性&#xff0c;用来描述描边的样式&#xff0c;其中重要的属性 stroke-dasharray、stroke-dashoffset。理解了这两个属性的原理&#xff0c;才能理解描边动画实现的原理。 stroke-dasharray&#xff1a;将描边线变成虚线、其中实线和虚线部分…