diff --git a/com.unity.netcode.gameobjects/CHANGELOG.md b/com.unity.netcode.gameobjects/CHANGELOG.md index 42f01110dc..a4df4be6d5 100644 --- a/com.unity.netcode.gameobjects/CHANGELOG.md +++ b/com.unity.netcode.gameobjects/CHANGELOG.md @@ -19,6 +19,7 @@ Additional documentation and release notes are available at [Multiplayer Documen - Fixed some errors that could occur if a connection is lost and the loss is detected when attempting to write to the socket. (#2495) ## Changed +- Improved performance of NetworkBehaviour initialization by replacing reflection when initializing NetworkVariables with compile-time code generation, which should help reduce hitching during additive scene loads. (#2522) ## [1.4.0] - 2023-04-10 diff --git a/com.unity.netcode.gameobjects/Editor/CodeGen/CodeGenHelpers.cs b/com.unity.netcode.gameobjects/Editor/CodeGen/CodeGenHelpers.cs index 7571857a54..0f28e01783 100644 --- a/com.unity.netcode.gameobjects/Editor/CodeGen/CodeGenHelpers.cs +++ b/com.unity.netcode.gameobjects/Editor/CodeGen/CodeGenHelpers.cs @@ -10,6 +10,7 @@ using Unity.CompilationPipeline.Common.Diagnostics; using Unity.CompilationPipeline.Common.ILPostProcessing; using UnityEngine; +using Object = System.Object; namespace Unity.Netcode.Editor.CodeGen { @@ -112,6 +113,60 @@ public static string FullNameWithGenericParameters(this TypeReference typeRefere return name; } + public static TypeReference MakeGenericType(this TypeReference self, params TypeReference[] arguments) + { + if (self.GenericParameters.Count != arguments.Length) + { + throw new ArgumentException(); + } + + var instance = new GenericInstanceType(self); + foreach (var argument in arguments) + { + instance.GenericArguments.Add(argument); + } + + return instance; + } + + public static MethodReference MakeGeneric(this MethodReference self, params TypeReference[] arguments) + { + var reference = new MethodReference(self.Name, self.ReturnType) + { + DeclaringType = self.DeclaringType.MakeGenericType(arguments), + HasThis = self.HasThis, + ExplicitThis = self.ExplicitThis, + CallingConvention = self.CallingConvention, + }; + + foreach (var parameter in self.Parameters) + { + reference.Parameters.Add(new ParameterDefinition(parameter.ParameterType)); + } + + foreach (var generic_parameter in self.GenericParameters) + { + reference.GenericParameters.Add(new GenericParameter(generic_parameter.Name, reference)); + } + + return reference; + } + + public static bool IsSubclassOf(this TypeReference typeReference, TypeReference baseClass) + { + var type = typeReference.Resolve(); + if (type.BaseType == null || type.BaseType.Name == nameof(Object)) + { + return false; + } + + if (type.BaseType.Resolve() == baseClass.Resolve()) + { + return true; + } + + return type.BaseType.IsSubclassOf(baseClass); + } public static bool HasInterface(this TypeReference typeReference, string interfaceTypeFullName) { diff --git a/com.unity.netcode.gameobjects/Editor/CodeGen/NetworkBehaviourILPP.cs b/com.unity.netcode.gameobjects/Editor/CodeGen/NetworkBehaviourILPP.cs index b57cfa4f12..2882fbbccb 100644 --- a/com.unity.netcode.gameobjects/Editor/CodeGen/NetworkBehaviourILPP.cs +++ b/com.unity.netcode.gameobjects/Editor/CodeGen/NetworkBehaviourILPP.cs @@ -166,6 +166,11 @@ private void CreateNetworkVariableTypeInitializers(AssemblyDefinition assembly) foreach (var type in m_WrappedNetworkVariableTypes) { + if (type.Resolve() == null) + { + continue; + } + if (IsSpecialCaseType(type)) { continue; @@ -251,11 +256,15 @@ private void CreateNetworkVariableTypeInitializers(AssemblyDefinition assembly) private FieldReference m_NetworkManager_rpc_name_table_FieldRef; private MethodReference m_NetworkManager_rpc_name_table_Add_MethodRef; private TypeReference m_NetworkBehaviour_TypeRef; + private TypeReference m_NetworkVariableBase_TypeRef; + private MethodReference m_NetworkVariableBase_Initialize_MethodRef; + private MethodReference m_NetworkBehaviour___nameNetworkVariable_MethodRef; private MethodReference m_NetworkBehaviour_beginSendServerRpc_MethodRef; private MethodReference m_NetworkBehaviour_endSendServerRpc_MethodRef; private MethodReference m_NetworkBehaviour_beginSendClientRpc_MethodRef; private MethodReference m_NetworkBehaviour_endSendClientRpc_MethodRef; private FieldReference m_NetworkBehaviour_rpc_exec_stage_FieldRef; + private FieldReference m_NetworkBehaviour_NetworkVariableFields_FieldRef; private MethodReference m_NetworkBehaviour_getNetworkManager_MethodRef; private MethodReference m_NetworkBehaviour_getOwnerClientId_MethodRef; private MethodReference m_NetworkHandlerDelegateCtor_MethodRef; @@ -275,6 +284,9 @@ private void CreateNetworkVariableTypeInitializers(AssemblyDefinition assembly) private MethodReference m_NetworkVariableSerializationTypes_InitializeEqualityChecker_UnmanagedValueEquals_MethodRef; private MethodReference m_NetworkVariableSerializationTypes_InitializeEqualityChecker_ManagedClassEquals_MethodRef; + private MethodReference m_ExceptionCtorMethodReference; + private MethodReference m_List_NetworkVariableBase_Add; + private MethodReference m_BytePacker_WriteValueBitPacked_Short_MethodRef; private MethodReference m_BytePacker_WriteValueBitPacked_UShort_MethodRef; private MethodReference m_BytePacker_WriteValueBitPacked_Int_MethodRef; @@ -348,12 +360,17 @@ private void CreateNetworkVariableTypeInitializers(AssemblyDefinition assembly) private const string k_NetworkManager_rpc_name_table = nameof(NetworkManager.__rpc_name_table); private const string k_NetworkBehaviour_rpc_exec_stage = nameof(NetworkBehaviour.__rpc_exec_stage); + private const string k_NetworkBehaviour_NetworkVariableFields = nameof(NetworkBehaviour.NetworkVariableFields); private const string k_NetworkBehaviour_beginSendServerRpc = nameof(NetworkBehaviour.__beginSendServerRpc); private const string k_NetworkBehaviour_endSendServerRpc = nameof(NetworkBehaviour.__endSendServerRpc); private const string k_NetworkBehaviour_beginSendClientRpc = nameof(NetworkBehaviour.__beginSendClientRpc); private const string k_NetworkBehaviour_endSendClientRpc = nameof(NetworkBehaviour.__endSendClientRpc); + private const string k_NetworkBehaviour___initializeVariables = nameof(NetworkBehaviour.__initializeVariables); private const string k_NetworkBehaviour_NetworkManager = nameof(NetworkBehaviour.NetworkManager); private const string k_NetworkBehaviour_OwnerClientId = nameof(NetworkBehaviour.OwnerClientId); + private const string k_NetworkBehaviour___nameNetworkVariable = nameof(NetworkBehaviour.__nameNetworkVariable); + + private const string k_NetworkVariableBase_Initialize = nameof(NetworkVariableBase.Initialize); private const string k_RpcAttribute_Delivery = nameof(RpcAttribute.Delivery); private const string k_ServerRpcAttribute_RequireOwnership = nameof(ServerRpcAttribute.RequireOwnership); @@ -379,6 +396,7 @@ private bool ImportReferences(ModuleDefinition moduleDefinition) TypeDefinition networkManagerTypeDef = null; TypeDefinition networkBehaviourTypeDef = null; + TypeDefinition networkVariableBaseTypeDef = null; TypeDefinition networkHandlerDelegateTypeDef = null; TypeDefinition rpcParamsTypeDef = null; TypeDefinition serverRpcParamsTypeDef = null; @@ -402,6 +420,12 @@ private bool ImportReferences(ModuleDefinition moduleDefinition) continue; } + if (networkVariableBaseTypeDef == null && netcodeTypeDef.Name == nameof(NetworkVariableBase)) + { + networkVariableBaseTypeDef = netcodeTypeDef; + continue; + } + if (networkHandlerDelegateTypeDef == null && netcodeTypeDef.Name == nameof(NetworkManager.RpcReceiveHandler)) { networkHandlerDelegateTypeDef = netcodeTypeDef; @@ -548,6 +572,9 @@ private bool ImportReferences(ModuleDefinition moduleDefinition) case k_NetworkBehaviour_endSendClientRpc: m_NetworkBehaviour_endSendClientRpc_MethodRef = moduleDefinition.ImportReference(methodDef); break; + case k_NetworkBehaviour___nameNetworkVariable: + m_NetworkBehaviour___nameNetworkVariable_MethodRef = moduleDefinition.ImportReference(methodDef); + break; } } @@ -558,6 +585,21 @@ private bool ImportReferences(ModuleDefinition moduleDefinition) case k_NetworkBehaviour_rpc_exec_stage: m_NetworkBehaviour_rpc_exec_stage_FieldRef = moduleDefinition.ImportReference(fieldDef); break; + case k_NetworkBehaviour_NetworkVariableFields: + m_NetworkBehaviour_NetworkVariableFields_FieldRef = moduleDefinition.ImportReference(fieldDef); + break; + } + } + + + m_NetworkVariableBase_TypeRef = moduleDefinition.ImportReference(networkVariableBaseTypeDef); + foreach (var methodDef in networkVariableBaseTypeDef.Methods) + { + switch (methodDef.Name) + { + case k_NetworkVariableBase_Initialize: + m_NetworkVariableBase_Initialize_MethodRef = moduleDefinition.ImportReference(methodDef); + break; } } @@ -785,6 +827,16 @@ private bool ImportReferences(ModuleDefinition moduleDefinition) } } + // Standard types are really hard to reliably find using the Mono Cecil way, they resolve differently in Mono vs .NET Core + // Importing with typeof() is less dangerous for standard framework types though, so we can just do it + var exceptionType = typeof(Exception); + var exceptionCtor = exceptionType.GetConstructor(new[] { typeof(string) }); + m_ExceptionCtorMethodReference = m_MainModule.ImportReference(exceptionCtor); + + var listType = typeof(List); + var addMethod = listType.GetMethod(nameof(List.Add), new[] { typeof(NetworkVariableBase) }); + m_List_NetworkVariableBase_Add = moduleDefinition.ImportReference(addMethod); + return true; } @@ -931,6 +983,8 @@ private void ProcessNetworkBehaviour(TypeDefinition typeDefinition, string[] ass } } + GenerateVariableInitialization(typeDefinition); + if (!typeDefinition.HasGenericParameters && !typeDefinition.IsGenericInstance) { var fieldTypes = new List(); @@ -1889,6 +1943,132 @@ private void InjectWriteAndCallBlocks(MethodDefinition methodDefinition, CustomA instructions.ForEach(instruction => processor.Body.Instructions.Insert(0, instruction)); } + private void GenerateVariableInitialization(TypeDefinition type) + { + foreach (var methodDefinition in type.Methods) + { + if (methodDefinition.Name == k_NetworkBehaviour___initializeVariables) + { + // If this hits, we've already generated the method for this class because a child class got processed first. + return; + } + } + + var method = new MethodDefinition( + k_NetworkBehaviour___initializeVariables, + MethodAttributes.Family | MethodAttributes.Virtual | MethodAttributes.HideBySig, + m_MainModule.TypeSystem.Void); + + var processor = method.Body.GetILProcessor(); + + method.Body.Variables.Add(new VariableDefinition(m_MainModule.TypeSystem.Boolean)); + + processor.Emit(OpCodes.Nop); + + foreach (var fieldDefinition in type.Fields) + { + FieldReference field = fieldDefinition; + if (type.HasGenericParameters) + { + var genericType = new GenericInstanceType(fieldDefinition.DeclaringType); + foreach (var parameter in fieldDefinition.DeclaringType.GenericParameters) + { + genericType.GenericArguments.Add(parameter); + } + field = new FieldReference(fieldDefinition.Name, fieldDefinition.FieldType, genericType); + } + if (field.FieldType.IsSubclassOf(m_NetworkVariableBase_TypeRef)) + { + // if({variable} != null) { + processor.Emit(OpCodes.Ldarg_0); + processor.Emit(OpCodes.Ldfld, field); + processor.Emit(OpCodes.Ldnull); + processor.Emit(OpCodes.Ceq); + processor.Emit(OpCodes.Stloc_0); + processor.Emit(OpCodes.Ldloc_0); + + var afterThrowInstruction = processor.Create(OpCodes.Nop); + + processor.Emit(OpCodes.Brfalse, afterThrowInstruction); + + // throw new Exception("..."); + processor.Emit(OpCodes.Nop); + processor.Emit(OpCodes.Ldstr, $"{type.Name}.{field.Name} cannot be null. All {nameof(NetworkVariableBase)} instances must be initialized."); + processor.Emit(OpCodes.Newobj, m_ExceptionCtorMethodReference); + processor.Emit(OpCodes.Throw); + + // } + processor.Append(afterThrowInstruction); + + // {variable}.Initialize(this); + processor.Emit(OpCodes.Ldarg_0); + processor.Emit(OpCodes.Ldfld, field); + processor.Emit(OpCodes.Ldarg_0); + processor.Emit(OpCodes.Callvirt, m_NetworkVariableBase_Initialize_MethodRef); + + // __nameNetworkVariable({variable}, "{variable}"); + processor.Emit(OpCodes.Nop); + processor.Emit(OpCodes.Ldarg_0); + processor.Emit(OpCodes.Ldarg_0); + processor.Emit(OpCodes.Ldfld, field); + processor.Emit(OpCodes.Ldstr, field.Name.Replace("<", string.Empty).Replace(">k__BackingField", string.Empty)); + processor.Emit(OpCodes.Call, m_NetworkBehaviour___nameNetworkVariable_MethodRef); + + // NetworkVariableFields.Add({variable}); + processor.Emit(OpCodes.Nop); + processor.Emit(OpCodes.Ldarg_0); + processor.Emit(OpCodes.Ldfld, m_NetworkBehaviour_NetworkVariableFields_FieldRef); + processor.Emit(OpCodes.Ldarg_0); + processor.Emit(OpCodes.Ldfld, field); + processor.Emit(OpCodes.Callvirt, m_List_NetworkVariableBase_Add); + } + } + + // Find the base method... + MethodReference initializeVariablesBaseReference = null; + foreach (var methodDefinition in type.BaseType.Resolve().Methods) + { + if (methodDefinition.Name == k_NetworkBehaviour___initializeVariables) + { + initializeVariablesBaseReference = m_MainModule.ImportReference(methodDefinition); + break; + } + } + + if (initializeVariablesBaseReference == null) + { + // If we couldn't find it, we have to go ahead and add it. + // The base class could be in another assembly... that's ok, this won't + // actually save but it'll generate the same method the same way later, + // so this at least allows us to reference it. + GenerateVariableInitialization(type.BaseType.Resolve()); + foreach (var methodDefinition in type.BaseType.Resolve().Methods) + { + if (methodDefinition.Name == k_NetworkBehaviour___initializeVariables) + { + initializeVariablesBaseReference = m_MainModule.ImportReference(methodDefinition); + break; + } + } + } + + if (type.BaseType.Resolve().HasGenericParameters) + { + var baseTypeInstance = (GenericInstanceType)type.BaseType; + initializeVariablesBaseReference = initializeVariablesBaseReference.MakeGeneric(baseTypeInstance.GenericArguments.ToArray()); + } + + // base.__initializeVariables(); + processor.Emit(OpCodes.Nop); + processor.Emit(OpCodes.Ldarg_0); + processor.Emit(OpCodes.Call, initializeVariablesBaseReference); + processor.Emit(OpCodes.Nop); + + processor.Emit(OpCodes.Ret); + + type.Methods.Add(method); + } + private MethodDefinition GenerateStaticHandler(MethodDefinition methodDefinition, CustomAttribute rpcAttribute, uint rpcMethodId) { var typeSystem = methodDefinition.Module.TypeSystem; diff --git a/com.unity.netcode.gameobjects/Editor/CodeGen/RuntimeAccessModifiersILPP.cs b/com.unity.netcode.gameobjects/Editor/CodeGen/RuntimeAccessModifiersILPP.cs index d16f10d4ba..0d9d2d0230 100644 --- a/com.unity.netcode.gameobjects/Editor/CodeGen/RuntimeAccessModifiersILPP.cs +++ b/com.unity.netcode.gameobjects/Editor/CodeGen/RuntimeAccessModifiersILPP.cs @@ -112,7 +112,7 @@ private void ProcessNetworkBehaviour(TypeDefinition typeDefinition) foreach (var fieldDefinition in typeDefinition.Fields) { - if (fieldDefinition.Name == nameof(NetworkBehaviour.__rpc_exec_stage)) + if (fieldDefinition.Name == nameof(NetworkBehaviour.__rpc_exec_stage) || fieldDefinition.Name == nameof(NetworkBehaviour.NetworkVariableFields)) { fieldDefinition.IsFamily = true; } @@ -123,7 +123,7 @@ private void ProcessNetworkBehaviour(TypeDefinition typeDefinition) if (methodDefinition.Name == nameof(NetworkBehaviour.__beginSendServerRpc) || methodDefinition.Name == nameof(NetworkBehaviour.__endSendServerRpc) || methodDefinition.Name == nameof(NetworkBehaviour.__beginSendClientRpc) || - methodDefinition.Name == nameof(NetworkBehaviour.__endSendClientRpc)) + methodDefinition.Name == nameof(NetworkBehaviour.__endSendClientRpc) || methodDefinition.Name == nameof(NetworkBehaviour.__initializeVariables) || methodDefinition.Name == nameof(NetworkBehaviour.__nameNetworkVariable)) { methodDefinition.IsFamily = true; } diff --git a/com.unity.netcode.gameobjects/Runtime/Core/NetworkBehaviour.cs b/com.unity.netcode.gameobjects/Runtime/Core/NetworkBehaviour.cs index 6a3b1d6ad1..0b0653bc64 100644 --- a/com.unity.netcode.gameobjects/Runtime/Core/NetworkBehaviour.cs +++ b/com.unity.netcode.gameobjects/Runtime/Core/NetworkBehaviour.cs @@ -1,7 +1,5 @@ using System; using System.Collections.Generic; -using System.Linq; -using System.Reflection; using Unity.Collections; using UnityEngine; @@ -551,35 +549,25 @@ public virtual void OnNetworkObjectParentChanged(NetworkObject parentNetworkObje private readonly List> m_DeliveryMappedNetworkVariableIndices = new List>(); private readonly List m_DeliveryTypesForNetworkVariableGroups = new List(); - internal readonly List NetworkVariableFields = new List(); - private static Dictionary s_FieldTypes = new Dictionary(); + // RuntimeAccessModifiersILPP will make this `protected` + internal readonly List NetworkVariableFields = new List(); - private static FieldInfo[] GetFieldInfoForType(Type type) +#pragma warning disable IDE1006 // disable naming rule violation check + // RuntimeAccessModifiersILPP will make this `protected` + internal virtual void __initializeVariables() +#pragma warning restore IDE1006 // restore naming rule violation check { - if (!s_FieldTypes.ContainsKey(type)) - { - s_FieldTypes.Add(type, GetFieldInfoForTypeRecursive(type)); - } - - return s_FieldTypes[type]; + // ILPP generates code for all NetworkBehaviour subtypes to initialize each type's network variables. } - private static FieldInfo[] GetFieldInfoForTypeRecursive(Type type, List list = null) +#pragma warning disable IDE1006 // disable naming rule violation check + // RuntimeAccessModifiersILPP will make this `protected` + // Using this method here because ILPP doesn't seem to let us do visibility modification on properties. + internal void __nameNetworkVariable(NetworkVariableBase variable, string varName) +#pragma warning restore IDE1006 // restore naming rule violation check { - if (list == null) - { - list = new List(); - } - - list.AddRange(type.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly)); - - if (type.BaseType != null && type.BaseType != typeof(NetworkBehaviour)) - { - return GetFieldInfoForTypeRecursive(type.BaseType, list); - } - - return list.OrderBy(x => x.Name, StringComparer.Ordinal).ToArray(); + variable.Name = varName; } internal void InitializeVariables() @@ -591,22 +579,7 @@ internal void InitializeVariables() m_VarInit = true; - var sortedFields = GetFieldInfoForType(GetType()); - for (int i = 0; i < sortedFields.Length; i++) - { - var fieldType = sortedFields[i].FieldType; - if (fieldType.IsSubclassOf(typeof(NetworkVariableBase))) - { - var instance = (NetworkVariableBase)sortedFields[i].GetValue(this) ?? throw new Exception($"{GetType().FullName}.{sortedFields[i].Name} cannot be null. All {nameof(NetworkVariableBase)} instances must be initialized."); - instance.Initialize(this); - - var instanceNameProperty = fieldType.GetProperty(nameof(NetworkVariableBase.Name)); - var sanitizedVariableName = sortedFields[i].Name.Replace("<", string.Empty).Replace(">k__BackingField", string.Empty); - instanceNameProperty?.SetValue(instance, sanitizedVariableName); - - NetworkVariableFields.Add(instance); - } - } + __initializeVariables(); { // Create index map for delivery types diff --git a/com.unity.netcode.gameobjects/Tests/Runtime/NetworkVariableTests.cs b/com.unity.netcode.gameobjects/Tests/Runtime/NetworkVariableTests.cs index b20d52a559..4faaa73c84 100644 --- a/com.unity.netcode.gameobjects/Tests/Runtime/NetworkVariableTests.cs +++ b/com.unity.netcode.gameobjects/Tests/Runtime/NetworkVariableTests.cs @@ -50,7 +50,12 @@ public class TemplateNetworkBehaviourType : NetworkBehaviour public NetworkVariable TheVar; } - public class ClassHavingNetworkBehaviour : TemplateNetworkBehaviourType + public class IntermediateNetworkBehavior : TemplateNetworkBehaviourType + { + public NetworkVariable TheVar2; + } + + public class ClassHavingNetworkBehaviour : IntermediateNetworkBehavior { } @@ -918,12 +923,16 @@ public void TestNetworkVariableTemplateBehaviourClass([Values(true, false)] bool bool VerifyClass() { - return m_Player1OnClient1.GetComponent().TheVar.Value != null && m_Player1OnClient1.GetComponent().TheVar.Value.SomeBool == m_Player1OnServer.GetComponent().TheVar.Value.SomeBool && - m_Player1OnClient1.GetComponent().TheVar.Value.SomeInt == m_Player1OnServer.GetComponent().TheVar.Value.SomeInt; + return (m_Player1OnClient1.GetComponent().TheVar.Value != null && m_Player1OnClient1.GetComponent().TheVar.Value.SomeBool == m_Player1OnServer.GetComponent().TheVar.Value.SomeBool && + m_Player1OnClient1.GetComponent().TheVar.Value.SomeInt == m_Player1OnServer.GetComponent().TheVar.Value.SomeInt) + && (m_Player1OnClient1.GetComponent().TheVar2.Value != null && m_Player1OnClient1.GetComponent().TheVar2.Value.SomeBool == m_Player1OnServer.GetComponent().TheVar2.Value.SomeBool && + m_Player1OnClient1.GetComponent().TheVar2.Value.SomeInt == m_Player1OnServer.GetComponent().TheVar2.Value.SomeInt); } m_Player1OnServer.GetComponent().TheVar.Value = new TestClass { SomeInt = k_TestUInt, SomeBool = false }; + m_Player1OnServer.GetComponent().TheVar2.Value = new TestClass { SomeInt = k_TestUInt, SomeBool = false }; m_Player1OnServer.GetComponent().TheVar.SetDirty(true); + m_Player1OnServer.GetComponent().TheVar2.SetDirty(true); // Wait for the client-side to notify it is finished initializing and spawning. Assert.True(WaitForConditionOrTimeOutWithTimeTravel(VerifyClass));