From 36d07fad5e18b02412dc27f39a0f1fe7a20fe024 Mon Sep 17 00:00:00 2001 From: Unity Technologies <@unity> Date: Mon, 4 Jan 2021 00:00:00 +0000 Subject: [PATCH] com.unity.netcode.gameobjects@1.0.0-pre.4 The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). Additional documentation and release notes are available at [Multiplayer Documentation](https://docs-multiplayer.unity3d.com). ## [1.0.0-pre.4] - 2021-01-04 ### Added - Added `com.unity.modules.physics` and `com.unity.modules.physics2d` package dependencies (#1565) ### Removed - Removed `com.unity.modules.ai` package dependency (#1565) - Removed `FixedQueue`, `StreamExtensions`, `TypeExtensions` (#1398) ### Fixed - Fixed in-scene NetworkObjects that are moved into the DDOL scene not getting restored to their original active state (enabled/disabled) after a full scene transition (#1354) - Fixed invalid IL code being generated when using `this` instead of `this ref` for the FastBufferReader/FastBufferWriter parameter of an extension method. (#1393) - Fixed an issue where if you are running as a server (not host) the LoadEventCompleted and UnloadEventCompleted events would fire early by the NetworkSceneManager (#1379) - Fixed a runtime error when sending an array of an INetworkSerializable type that's implemented as a struct (#1402) - NetworkConfig will no longer throw an OverflowException in GetConfig() when ForceSamePrefabs is enabled and the number of prefabs causes the config blob size to exceed 1300 bytes. (#1385) - Fixed NetworkVariable not calling NetworkSerialize on INetworkSerializable types (#1383) - Fixed NullReferenceException on ImportReferences call in NetworkBehaviourILPP (#1434) - Fixed NetworkObjects not being despawned before they are destroyed during shutdown for client, host, and server instances. (#1390) - Fixed KeyNotFound exception when removing ownership of a newly spawned NetworkObject that is already owned by the server. (#1500) - Fixed NetworkManager.LocalClient not being set when starting as a host. (#1511) - Fixed a few memory leak cases when shutting down NetworkManager during Incoming Message Queue processing. (#1323) ### Changed - The SDK no longer limits message size to 64k. (The transport may still impose its own limits, but the SDK no longer does.) (#1384) - Updated com.unity.collections to 1.1.0 (#1451) --- CHANGELOG.md | 28 ++ Components/NetworkRigidbody2D.cs | 3 +- Components/NetworkTransform.cs | 2 +- Editor/CodeGen/CodeGenHelpers.cs | 22 ++ Editor/CodeGen/INetworkSerializableILPP.cs | 167 ++++++++ .../CodeGen/INetworkSerializableILPP.cs.meta | 3 + Editor/CodeGen/NetworkBehaviourILPP.cs | 50 ++- Editor/CodeGen/RuntimeAccessModifiersILPP.cs | 14 + Editor/NetworkManagerEditor.cs | 2 +- Runtime/Collections.meta | 8 - Runtime/Collections/FixedQueue.cs | 77 ---- Runtime/Configuration/NetworkConfig.cs | 8 +- Runtime/Configuration/NetworkConstants.cs | 2 +- Runtime/Core/NetworkBehaviour.cs | 14 +- Runtime/Core/NetworkManager.cs | 71 +++- .../Core/{SnapshotRTT.cs => SnapshotRtt.cs} | 0 ...napshotRTT.cs.meta => SnapshotRtt.cs.meta} | 0 Runtime/Core/SnapshotSystem.cs | 374 +++++++++++------- Runtime/Messaging/CustomMessageManager.cs | 8 +- Runtime/Messaging/MessageHeader.cs | 5 +- Runtime/Messaging/Messages/NamedMessage.cs | 2 +- .../Messaging/Messages/SnapshotDataMessage.cs | 29 +- Runtime/Messaging/Messages/UnnamedMessage.cs | 2 +- Runtime/Messaging/MessagingSystem.cs | 166 ++++---- Runtime/Messaging/NetworkContext.cs | 5 + Runtime/Metrics/StreamExtensions.cs | 12 - Runtime/NetworkVariable/NetworkVariable.cs | 55 ++- .../NetworkVariable/NetworkVariableHelper.cs | 22 ++ .../NetworkVariableHelper.cs.meta} | 2 +- Runtime/Reflection.meta | 8 - Runtime/Reflection/TypeExtensions.cs | 31 -- .../SceneManagement/NetworkSceneManager.cs | 41 +- Runtime/Serialization/FastBufferWriter.cs | 4 +- Runtime/Spawning/NetworkSpawnManager.cs | 122 ++++-- .../Editor/Messaging/MessageReceivingTests.cs | 29 +- Tests/Editor/Messaging/MessageSendingTests.cs | 31 +- Tests/Editor/SnapshotRttTests.cs | 4 +- Tests/Editor/SnapshotTests.cs | 372 +++++++++++++++++ .../Editor/SnapshotTests.cs.meta | 2 +- Tests/Editor/StartStopTests.cs | 15 +- .../com.unity.netcode.editortests.asmdef | 1 + .../Runtime/Metrics/MessagingMetricsTests.cs | 6 +- .../Metrics/OwnershipChangeMetricsTests.cs | 4 +- .../Runtime/Metrics/ServerLogsMetricTests.cs | 4 +- .../Metrics/TransportBytesMetricsTests.cs | 4 +- Tests/Runtime/MultiInstanceHelpers.cs | 2 +- Tests/Runtime/NetworkBehaviourUpdaterTests.cs | 2 + .../NetworkObjectOnNetworkDespawnTests.cs | 132 +++++++ ...NetworkObjectOnNetworkDespawnTests.cs.meta | 2 +- .../NetworkObjectOwnershipTests.cs | 3 + Tests/Runtime/NetworkVariableTests.cs | 32 ++ .../Runtime/Physics/NetworkRigidbody2DTest.cs | 1 + Tests/Runtime/RpcPipelineTestComponent.cs | 148 ------- Tests/Runtime/RpcTests.cs | 18 +- Tests/Runtime/StopStartRuntimeTests.cs | 73 ++++ ....cs.meta => StopStartRuntimeTests.cs.meta} | 2 +- Tests/Runtime/Timing/TimeMultiInstanceTest.cs | 4 +- Tests/Runtime/Transport/SIPTransport.cs | 1 + package.json | 11 +- 59 files changed, 1583 insertions(+), 679 deletions(-) create mode 100644 Editor/CodeGen/INetworkSerializableILPP.cs create mode 100644 Editor/CodeGen/INetworkSerializableILPP.cs.meta delete mode 100644 Runtime/Collections.meta delete mode 100644 Runtime/Collections/FixedQueue.cs rename Runtime/Core/{SnapshotRTT.cs => SnapshotRtt.cs} (100%) rename Runtime/Core/{SnapshotRTT.cs.meta => SnapshotRtt.cs.meta} (100%) delete mode 100644 Runtime/Metrics/StreamExtensions.cs create mode 100644 Runtime/NetworkVariable/NetworkVariableHelper.cs rename Runtime/{Reflection/TypeExtensions.cs.meta => NetworkVariable/NetworkVariableHelper.cs.meta} (83%) delete mode 100644 Runtime/Reflection.meta delete mode 100644 Runtime/Reflection/TypeExtensions.cs create mode 100644 Tests/Editor/SnapshotTests.cs rename Runtime/Metrics/StreamExtensions.cs.meta => Tests/Editor/SnapshotTests.cs.meta (83%) create mode 100644 Tests/Runtime/NetworkObject/NetworkObjectOnNetworkDespawnTests.cs rename Runtime/Collections/FixedQueue.cs.meta => Tests/Runtime/NetworkObject/NetworkObjectOnNetworkDespawnTests.cs.meta (83%) delete mode 100644 Tests/Runtime/RpcPipelineTestComponent.cs create mode 100644 Tests/Runtime/StopStartRuntimeTests.cs rename Tests/Runtime/{RpcPipelineTestComponent.cs.meta => StopStartRuntimeTests.cs.meta} (83%) diff --git a/CHANGELOG.md b/CHANGELOG.md index b7462ed..5b925fc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,34 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) Additional documentation and release notes are available at [Multiplayer Documentation](https://docs-multiplayer.unity3d.com). +## [1.0.0-pre.4] - 2021-01-04 + +### Added + +- Added `com.unity.modules.physics` and `com.unity.modules.physics2d` package dependencies (#1565) + +### Removed + +- Removed `com.unity.modules.ai` package dependency (#1565) +- Removed `FixedQueue`, `StreamExtensions`, `TypeExtensions` (#1398) + +### Fixed +- Fixed in-scene NetworkObjects that are moved into the DDOL scene not getting restored to their original active state (enabled/disabled) after a full scene transition (#1354) +- Fixed invalid IL code being generated when using `this` instead of `this ref` for the FastBufferReader/FastBufferWriter parameter of an extension method. (#1393) +- Fixed an issue where if you are running as a server (not host) the LoadEventCompleted and UnloadEventCompleted events would fire early by the NetworkSceneManager (#1379) +- Fixed a runtime error when sending an array of an INetworkSerializable type that's implemented as a struct (#1402) +- NetworkConfig will no longer throw an OverflowException in GetConfig() when ForceSamePrefabs is enabled and the number of prefabs causes the config blob size to exceed 1300 bytes. (#1385) +- Fixed NetworkVariable not calling NetworkSerialize on INetworkSerializable types (#1383) +- Fixed NullReferenceException on ImportReferences call in NetworkBehaviourILPP (#1434) +- Fixed NetworkObjects not being despawned before they are destroyed during shutdown for client, host, and server instances. (#1390) +- Fixed KeyNotFound exception when removing ownership of a newly spawned NetworkObject that is already owned by the server. (#1500) +- Fixed NetworkManager.LocalClient not being set when starting as a host. (#1511) +- Fixed a few memory leak cases when shutting down NetworkManager during Incoming Message Queue processing. (#1323) + +### Changed +- The SDK no longer limits message size to 64k. (The transport may still impose its own limits, but the SDK no longer does.) (#1384) +- Updated com.unity.collections to 1.1.0 (#1451) + ## [1.0.0-pre.3] - 2021-10-22 ### Added diff --git a/Components/NetworkRigidbody2D.cs b/Components/NetworkRigidbody2D.cs index e0997da..b9c809d 100644 --- a/Components/NetworkRigidbody2D.cs +++ b/Components/NetworkRigidbody2D.cs @@ -32,7 +32,7 @@ private void Awake() private void FixedUpdate() { - if (NetworkManager.IsListening) + if (IsSpawned) { if (HasAuthority != m_IsAuthority) { @@ -74,7 +74,6 @@ public override void OnNetworkSpawn() /// public override void OnNetworkDespawn() { - m_IsAuthority = false; UpdateRigidbodyKinematicMode(); } } diff --git a/Components/NetworkTransform.cs b/Components/NetworkTransform.cs index 831d1e2..e7a4e79 100644 --- a/Components/NetworkTransform.cs +++ b/Components/NetworkTransform.cs @@ -807,7 +807,7 @@ private void SetStateServerRpc(Vector3 pos, Quaternion rot, Vector3 scale, bool // conditional to users only making transform update changes in FixedUpdate. protected virtual void Update() { - if (!NetworkObject.IsSpawned) + if (!IsSpawned) { return; } diff --git a/Editor/CodeGen/CodeGenHelpers.cs b/Editor/CodeGen/CodeGenHelpers.cs index 390d190..6e5e1ff 100644 --- a/Editor/CodeGen/CodeGenHelpers.cs +++ b/Editor/CodeGen/CodeGenHelpers.cs @@ -264,6 +264,28 @@ public static void AddError(this List diagnostics, SequencePo }); } + public static void AddWarning(this List diagnostics, string message) + { + diagnostics.AddWarning((SequencePoint)null, message); + } + + public static void AddWarning(this List diagnostics, MethodDefinition methodDefinition, string message) + { + diagnostics.AddWarning(methodDefinition.DebugInformation.SequencePoints.FirstOrDefault(), message); + } + + public static void AddWarning(this List diagnostics, SequencePoint sequencePoint, string message) + { + diagnostics.Add(new DiagnosticMessage + { + DiagnosticType = DiagnosticType.Warning, + File = sequencePoint?.Document.Url.Replace($"{Environment.CurrentDirectory}{Path.DirectorySeparatorChar}", ""), + Line = sequencePoint?.StartLine ?? 0, + Column = sequencePoint?.StartColumn ?? 0, + MessageData = $" - {message}" + }); + } + public static void RemoveRecursiveReferences(this ModuleDefinition moduleDefinition) { // Weird behavior from Cecil: When importing a reference to a specific implementation of a generic diff --git a/Editor/CodeGen/INetworkSerializableILPP.cs b/Editor/CodeGen/INetworkSerializableILPP.cs new file mode 100644 index 0000000..49af82b --- /dev/null +++ b/Editor/CodeGen/INetworkSerializableILPP.cs @@ -0,0 +1,167 @@ +using System; +using System.IO; +using System.Linq; +using System.Collections.Generic; +using System.Reflection; +using Mono.Cecil; +using Mono.Cecil.Cil; +using Mono.Cecil.Rocks; +using Unity.CompilationPipeline.Common.Diagnostics; +using Unity.CompilationPipeline.Common.ILPostProcessing; +using ILPPInterface = Unity.CompilationPipeline.Common.ILPostProcessing.ILPostProcessor; +using MethodAttributes = Mono.Cecil.MethodAttributes; + +namespace Unity.Netcode.Editor.CodeGen +{ + + internal sealed class INetworkSerializableILPP : ILPPInterface + { + public override ILPPInterface GetInstance() => this; + + public override bool WillProcess(ICompiledAssembly compiledAssembly) => + compiledAssembly.Name == CodeGenHelpers.RuntimeAssemblyName || + compiledAssembly.References.Any(filePath => Path.GetFileNameWithoutExtension(filePath) == CodeGenHelpers.RuntimeAssemblyName); + + private readonly List m_Diagnostics = new List(); + + public override ILPostProcessResult Process(ICompiledAssembly compiledAssembly) + { + if (!WillProcess(compiledAssembly)) + { + return null; + } + + + m_Diagnostics.Clear(); + + // read + var assemblyDefinition = CodeGenHelpers.AssemblyDefinitionFor(compiledAssembly, out var resolver); + if (assemblyDefinition == null) + { + m_Diagnostics.AddError($"Cannot read assembly definition: {compiledAssembly.Name}"); + return null; + } + + // process + var mainModule = assemblyDefinition.MainModule; + if (mainModule != null) + { + try + { + if (ImportReferences(mainModule)) + { + var types = mainModule.GetTypes() + .Where(t => t.Resolve().HasInterface(CodeGenHelpers.INetworkSerializable_FullName) && !t.Resolve().IsAbstract && t.Resolve().IsValueType) + .ToList(); + // process `INetworkMessage` types + if (types.Count == 0) + { + return null; + } + + CreateModuleInitializer(assemblyDefinition, types); + } + else + { + m_Diagnostics.AddError($"Cannot import references into main module: {mainModule.Name}"); + } + } + catch (Exception e) + { + m_Diagnostics.AddError((e.ToString() + e.StackTrace.ToString()).Replace("\n", "|").Replace("\r", "|")); + } + } + else + { + m_Diagnostics.AddError($"Cannot get main module from assembly definition: {compiledAssembly.Name}"); + } + + mainModule.RemoveRecursiveReferences(); + + // write + var pe = new MemoryStream(); + var pdb = new MemoryStream(); + + var writerParameters = new WriterParameters + { + SymbolWriterProvider = new PortablePdbWriterProvider(), + SymbolStream = pdb, + WriteSymbols = true + }; + + assemblyDefinition.Write(pe, writerParameters); + + return new ILPostProcessResult(new InMemoryAssembly(pe.ToArray(), pdb.ToArray()), m_Diagnostics); + } + + private MethodReference m_InitializeDelegates_MethodRef; + + private const string k_InitializeMethodName = nameof(NetworkVariableHelper.InitializeDelegates); + + private bool ImportReferences(ModuleDefinition moduleDefinition) + { + + var helperType = typeof(NetworkVariableHelper); + foreach (var methodInfo in helperType.GetMethods(BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public)) + { + switch (methodInfo.Name) + { + case k_InitializeMethodName: + m_InitializeDelegates_MethodRef = moduleDefinition.ImportReference(methodInfo); + break; + } + } + return true; + } + + private MethodDefinition GetOrCreateStaticConstructor(TypeDefinition typeDefinition) + { + var staticCtorMethodDef = typeDefinition.GetStaticConstructor(); + if (staticCtorMethodDef == null) + { + staticCtorMethodDef = new MethodDefinition( + ".cctor", // Static Constructor (constant-constructor) + MethodAttributes.HideBySig | + MethodAttributes.SpecialName | + MethodAttributes.RTSpecialName | + MethodAttributes.Static, + typeDefinition.Module.TypeSystem.Void); + staticCtorMethodDef.Body.Instructions.Add(Instruction.Create(OpCodes.Ret)); + typeDefinition.Methods.Add(staticCtorMethodDef); + } + + return staticCtorMethodDef; + } + + // Creates a static module constructor (which is executed when the module is loaded) that registers all the + // message types in the assembly with MessagingSystem. + // This is the same behavior as annotating a static method with [ModuleInitializer] in standardized + // C# (that attribute doesn't exist in Unity, but the static module constructor still works) + // https://docs.microsoft.com/en-us/dotnet/api/system.runtime.compilerservices.moduleinitializerattribute?view=net-5.0 + // https://web.archive.org/web/20100212140402/http://blogs.msdn.com/junfeng/archive/2005/11/19/494914.aspx + private void CreateModuleInitializer(AssemblyDefinition assembly, List networkSerializableTypes) + { + foreach (var typeDefinition in assembly.MainModule.Types) + { + if (typeDefinition.FullName == "") + { + var staticCtorMethodDef = GetOrCreateStaticConstructor(typeDefinition); + + var processor = staticCtorMethodDef.Body.GetILProcessor(); + + var instructions = new List(); + + foreach (var type in networkSerializableTypes) + { + var method = new GenericInstanceMethod(m_InitializeDelegates_MethodRef); + method.GenericArguments.Add(type); + instructions.Add(processor.Create(OpCodes.Call, method)); + } + + instructions.ForEach(instruction => processor.Body.Instructions.Insert(processor.Body.Instructions.Count - 1, instruction)); + break; + } + } + } + } +} diff --git a/Editor/CodeGen/INetworkSerializableILPP.cs.meta b/Editor/CodeGen/INetworkSerializableILPP.cs.meta new file mode 100644 index 0000000..7043beb --- /dev/null +++ b/Editor/CodeGen/INetworkSerializableILPP.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 64a0c1e708fa46a389d64e7b4708e6c7 +timeCreated: 1635535237 \ No newline at end of file diff --git a/Editor/CodeGen/NetworkBehaviourILPP.cs b/Editor/CodeGen/NetworkBehaviourILPP.cs index d49d48c..f6de9f1 100644 --- a/Editor/CodeGen/NetworkBehaviourILPP.cs +++ b/Editor/CodeGen/NetworkBehaviourILPP.cs @@ -312,7 +312,11 @@ private bool ImportReferences(ModuleDefinition moduleDefinition) assemblies.Add(m_MainModule.Assembly); foreach (var reference in m_MainModule.AssemblyReferences) { - assemblies.Add(m_AssemblyResolver.Resolve(reference)); + var assembly = m_AssemblyResolver.Resolve(reference); + if (assembly != null) + { + assemblies.Add(assembly); + } } var extensionConstructor = @@ -575,21 +579,28 @@ private MethodReference GetFastBufferWriterWriteMethod(string name, TypeReferenc { if (method.GenericParameters[0].HasConstraints) { + var meetsConstraints = true; foreach (var constraint in method.GenericParameters[0].Constraints) { var resolvedConstraint = constraint.Resolve(); if ( (resolvedConstraint.IsInterface && - checkType.HasInterface(resolvedConstraint.FullName)) + !checkType.HasInterface(resolvedConstraint.FullName)) || (resolvedConstraint.IsClass && - checkType.Resolve().IsSubclassOf(resolvedConstraint.FullName))) + !checkType.Resolve().IsSubclassOf(resolvedConstraint.FullName)) + || (resolvedConstraint.Name == "ValueType" && !checkType.IsValueType)) { - var instanceMethod = new GenericInstanceMethod(method); - instanceMethod.GenericArguments.Add(checkType); - return instanceMethod; + meetsConstraints = false; + break; } } + if (meetsConstraints) + { + var instanceMethod = new GenericInstanceMethod(method); + instanceMethod.GenericArguments.Add(checkType); + return instanceMethod; + } } } } @@ -950,17 +961,23 @@ private void InjectWriteAndCallBlocks(MethodDefinition methodDefinition, CustomA // writer.WriteValueSafe(param) for value types, OR // writer.WriteValueSafe(param, -1, 0) for arrays of value types, OR // writer.WriteValueSafe(param, false) for strings - instructions.Add(processor.Create(OpCodes.Ldloca, serializerLocIdx)); var method = methodRef.Resolve(); var checkParameter = method.Parameters[0]; var isExtensionMethod = false; - if (checkParameter.ParameterType.Resolve() == - m_FastBufferWriter_TypeRef.MakeByReferenceType().Resolve()) + if (methodRef.Resolve().DeclaringType != m_FastBufferWriter_TypeRef.Resolve()) { isExtensionMethod = true; checkParameter = method.Parameters[1]; } - if (checkParameter.IsIn) + if (!isExtensionMethod || method.Parameters[0].ParameterType.IsByReference) + { + instructions.Add(processor.Create(OpCodes.Ldloca, serializerLocIdx)); + } + else + { + instructions.Add(processor.Create(OpCodes.Ldloc, serializerLocIdx)); + } + if (checkParameter.IsIn || checkParameter.IsOut || checkParameter.ParameterType.IsByReference) { instructions.Add(processor.Create(OpCodes.Ldarga, paramIndex + 1)); } @@ -1271,7 +1288,18 @@ private MethodDefinition GenerateStaticHandler(MethodDefinition methodDefinition if (foundMethodRef) { // reader.ReadValueSafe(out localVar); - processor.Emit(OpCodes.Ldarga, 1); + + var checkParameter = methodRef.Resolve().Parameters[0]; + + var isExtensionMethod = methodRef.Resolve().DeclaringType != m_FastBufferReader_TypeRef.Resolve(); + if (!isExtensionMethod || checkParameter.ParameterType.IsByReference) + { + processor.Emit(OpCodes.Ldarga, 1); + } + else + { + processor.Emit(OpCodes.Ldarg, 1); + } processor.Emit(OpCodes.Ldloca, localIndex); if (paramType == typeSystem.String) { diff --git a/Editor/CodeGen/RuntimeAccessModifiersILPP.cs b/Editor/CodeGen/RuntimeAccessModifiersILPP.cs index 1c3d846..17f9b50 100644 --- a/Editor/CodeGen/RuntimeAccessModifiersILPP.cs +++ b/Editor/CodeGen/RuntimeAccessModifiersILPP.cs @@ -52,6 +52,9 @@ public override ILPostProcessResult Process(ICompiledAssembly compiledAssembly) case nameof(NetworkBehaviour): ProcessNetworkBehaviour(typeDefinition); break; + case nameof(NetworkVariableHelper): + ProcessNetworkVariableHelper(typeDefinition); + break; case nameof(__RpcParams): typeDefinition.IsPublic = true; break; @@ -100,6 +103,17 @@ private void ProcessNetworkManager(TypeDefinition typeDefinition, string[] assem } } + private void ProcessNetworkVariableHelper(TypeDefinition typeDefinition) + { + foreach (var methodDefinition in typeDefinition.Methods) + { + if (methodDefinition.Name == nameof(NetworkVariableHelper.InitializeDelegates)) + { + methodDefinition.IsPublic = true; + } + } + } + private void ProcessNetworkBehaviour(TypeDefinition typeDefinition) { foreach (var nestedType in typeDefinition.NestedTypes) diff --git a/Editor/NetworkManagerEditor.cs b/Editor/NetworkManagerEditor.cs index 63342c1..6cb2ccd 100644 --- a/Editor/NetworkManagerEditor.cs +++ b/Editor/NetworkManagerEditor.cs @@ -363,7 +363,7 @@ private static void DrawInstallMultiplayerToolsTip() const string getToolsText = "Access additional tools for multiplayer development by installing the Multiplayer Tools package in the Package Manager."; const string openDocsButtonText = "Open Docs"; const string dismissButtonText = "Dismiss"; - const string targetUrl = "https://docs-multiplayer.unity3d.com/docs/tutorials/goldenpath_series/goldenpath_foundation_module"; + const string targetUrl = "https://docs-multiplayer.unity3d.com/docs/tools/install-tools"; const string infoIconName = "console.infoicon"; if (PlayerPrefs.GetInt(InstallMultiplayerToolsTipDismissedPlayerPrefKey, 0) != 0) diff --git a/Runtime/Collections.meta b/Runtime/Collections.meta deleted file mode 100644 index 6f36eb7..0000000 --- a/Runtime/Collections.meta +++ /dev/null @@ -1,8 +0,0 @@ -fileFormatVersion: 2 -guid: f2ef964afcae91248b2298b479ed1b53 -folderAsset: yes -DefaultImporter: - externalObjects: {} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Runtime/Collections/FixedQueue.cs b/Runtime/Collections/FixedQueue.cs deleted file mode 100644 index 1cbb4f1..0000000 --- a/Runtime/Collections/FixedQueue.cs +++ /dev/null @@ -1,77 +0,0 @@ -using System; - -namespace Unity.Netcode -{ - /// - /// Queue with a fixed size - /// - /// The type of the queue - public sealed class FixedQueue - { - private readonly T[] m_Queue; - private int m_QueueCount = 0; - private int m_QueueStart; - - /// - /// The amount of enqueued objects - /// - public int Count => m_QueueCount; - - /// - /// Gets the element at a given virtual index - /// - /// The virtual index to get the item from - /// The element at the virtual index - public T this[int index] => m_Queue[(m_QueueStart + index) % m_Queue.Length]; - - /// - /// Creates a new FixedQueue with a given size - /// - /// The size of the queue - public FixedQueue(int maxSize) - { - m_Queue = new T[maxSize]; - m_QueueStart = 0; - } - - /// - /// Enqueues an object - /// - /// - /// - public bool Enqueue(T t) - { - m_Queue[(m_QueueStart + m_QueueCount) % m_Queue.Length] = t; - if (++m_QueueCount > m_Queue.Length) - { - --m_QueueCount; - return true; - } - - return false; - } - - /// - /// Dequeues an object - /// - /// - public T Dequeue() - { - if (--m_QueueCount == -1) - { - throw new IndexOutOfRangeException("Cannot dequeue empty queue!"); - } - - T res = m_Queue[m_QueueStart]; - m_QueueStart = (m_QueueStart + 1) % m_Queue.Length; - return res; - } - - /// - /// Gets the element at a given virtual index - /// - /// The virtual index to get the item from - /// The element at the virtual index - public T ElementAt(int index) => m_Queue[(m_QueueStart + index) % m_Queue.Length]; - } -} diff --git a/Runtime/Configuration/NetworkConfig.cs b/Runtime/Configuration/NetworkConfig.cs index d938294..ccd628f 100644 --- a/Runtime/Configuration/NetworkConfig.cs +++ b/Runtime/Configuration/NetworkConfig.cs @@ -141,15 +141,15 @@ public class NetworkConfig /// /// Whether or not to enable Snapshot System for variable updates. Not supported in this version. /// - public bool UseSnapshotDelta { get; } = false; + public bool UseSnapshotDelta { get; internal set; } = false; /// /// Whether or not to enable Snapshot System for spawn and despawn commands. Not supported in this version. /// - public bool UseSnapshotSpawn { get; } = false; + public bool UseSnapshotSpawn { get; internal set; } = false; /// /// When Snapshot System spawn is enabled: max size of Snapshot Messages. Meant to fit MTU. /// - public int SnapshotMaxSpawnUsage { get; } = 1200; + public int SnapshotMaxSpawnUsage { get; } = 1000; public const int RttAverageSamples = 5; // number of RTT to keep an average of (plus one) public const int RttWindowSize = 64; // number of slots to use for RTT computations (max number of in-flight packets) @@ -224,7 +224,7 @@ public ulong GetConfig(bool cache = true) return m_ConfigHash.Value; } - var writer = new FastBufferWriter(MessagingSystem.NON_FRAGMENTED_MESSAGE_MAX_SIZE, Allocator.Temp); + var writer = new FastBufferWriter(MessagingSystem.NON_FRAGMENTED_MESSAGE_MAX_SIZE, Allocator.Temp, int.MaxValue); using (writer) { writer.WriteValueSafe(ProtocolVersion); diff --git a/Runtime/Configuration/NetworkConstants.cs b/Runtime/Configuration/NetworkConstants.cs index 55facaf..b9eed28 100644 --- a/Runtime/Configuration/NetworkConstants.cs +++ b/Runtime/Configuration/NetworkConstants.cs @@ -5,6 +5,6 @@ namespace Unity.Netcode /// internal static class NetworkConstants { - internal const string PROTOCOL_VERSION = "14.0.0"; + internal const string PROTOCOL_VERSION = "15.0.0"; } } diff --git a/Runtime/Core/NetworkBehaviour.cs b/Runtime/Core/NetworkBehaviour.cs index 0c13955..f271c4c 100644 --- a/Runtime/Core/NetworkBehaviour.cs +++ b/Runtime/Core/NetworkBehaviour.cs @@ -82,7 +82,8 @@ internal void __sendServerRpc(FastBufferWriter writer, uint rpcMethodId, ServerR SystemOwner = NetworkManager, // header information isn't valid since it's not a real message. // Passing false to canDefer prevents it being accessed. - Header = new MessageHeader() + Header = new MessageHeader(), + SerializedHeaderSize = 0, }; message.Handle(tempBuffer, context, NetworkManager, NetworkManager.ServerClientId, false); rpcMessageSize = tempBuffer.Length; @@ -188,7 +189,8 @@ internal unsafe void __sendClientRpc(FastBufferWriter writer, uint rpcMethodId, SystemOwner = NetworkManager, // header information isn't valid since it's not a real message. // Passing false to canDefer prevents it being accessed. - Header = new MessageHeader() + Header = new MessageHeader(), + SerializedHeaderSize = 0, }; message.Handle(tempBuffer, context, NetworkManager, NetworkManager.ServerClientId, false); messageSize = tempBuffer.Length; @@ -282,9 +284,13 @@ public NetworkObject NetworkObject m_NetworkObject = GetComponentInParent(); } - if (m_NetworkObject == null && NetworkLog.CurrentLogLevel <= LogLevel.Normal) + if (m_NetworkObject == null || NetworkManager.Singleton == null || + (NetworkManager.Singleton != null && !NetworkManager.Singleton.ShutdownInProgress)) { - NetworkLog.LogWarning($"Could not get {nameof(NetworkObject)} for the {nameof(NetworkBehaviour)}. Are you missing a {nameof(NetworkObject)} component?"); + if (NetworkLog.CurrentLogLevel < LogLevel.Normal) + { + NetworkLog.LogWarning($"Could not get {nameof(NetworkObject)} for the {nameof(NetworkBehaviour)}. Are you missing a {nameof(NetworkObject)} component?"); + } } return m_NetworkObject; diff --git a/Runtime/Core/NetworkManager.cs b/Runtime/Core/NetworkManager.cs index fe26534..097d9bb 100644 --- a/Runtime/Core/NetworkManager.cs +++ b/Runtime/Core/NetworkManager.cs @@ -73,6 +73,9 @@ public NetworkPrefabHandler PrefabHandler } } + private bool m_ShuttingDown; + private bool m_StopProcessingMessages; + private class NetworkManagerHooks : INetworkHooks { private NetworkManager m_NetworkManager; @@ -116,7 +119,7 @@ public void OnAfterReceiveBatch(ulong senderId, int messageCount, int batchSizeI public bool OnVerifyCanSend(ulong destinationId, Type messageType, NetworkDelivery delivery) { - return true; + return !m_NetworkManager.m_StopProcessingMessages; } public bool OnVerifyCanReceive(ulong senderId, Type messageType) @@ -134,7 +137,7 @@ public bool OnVerifyCanReceive(ulong senderId, Type messageType) return false; } - return true; + return !m_NetworkManager.m_StopProcessingMessages; } } @@ -333,6 +336,9 @@ public IReadOnlyList ConnectedClientsIds /// public bool IsConnectedClient { get; internal set; } + + public bool ShutdownInProgress { get { return m_ShuttingDown; } } + /// /// The callback to invoke once a client connects. This callback is only ran on the server and on the local client that connects. /// @@ -345,8 +351,6 @@ public IReadOnlyList ConnectedClientsIds /// public event Action OnClientDisconnectCallback = null; - internal void InvokeOnClientDisconnectCallback(ulong clientId) => OnClientDisconnectCallback?.Invoke(clientId); - /// /// The callback to invoke once the server is ready /// @@ -556,8 +560,6 @@ private void Initialize(bool server) SnapshotSystem = null; } - SnapshotSystem = new SnapshotSystem(this); - if (server) { NetworkTimeSystem = NetworkTimeSystem.ServerTimeSystem(); @@ -570,6 +572,8 @@ private void Initialize(bool server) NetworkTickSystem = new NetworkTickSystem(NetworkConfig.TickRate, 0, 0); NetworkTickSystem.Tick += OnNetworkManagerTick; + SnapshotSystem = new SnapshotSystem(this, NetworkConfig, NetworkTickSystem); + this.RegisterNetworkUpdate(NetworkUpdateStage.PreUpdate); // This is used to remove entries not needed or invalid @@ -976,7 +980,7 @@ private void OnApplicationQuit() // Note that this gets also called manually by OnSceneUnloaded and OnApplicationQuit private void OnDestroy() { - Shutdown(); + ShutdownInternal(); UnityEngine.SceneManagement.SceneManager.sceneUnloaded -= OnSceneUnloaded; @@ -996,13 +1000,30 @@ private void DisconnectRemoteClient(ulong clientId) /// Globally shuts down the library. /// Disconnects clients if connected and stops server if running. /// - public void Shutdown() + /// + /// If false, any messages that are currently in the incoming queue will be handled, + /// and any messages in the outgoing queue will be sent, before the shutdown is processed. + /// If true, NetworkManager will shut down immediately, and any unprocessed or unsent messages + /// will be discarded. + /// + public void Shutdown(bool discardMessageQueue = false) { if (NetworkLog.CurrentLogLevel <= LogLevel.Developer) { NetworkLog.LogInfo(nameof(Shutdown)); } + m_ShuttingDown = true; + m_StopProcessingMessages = discardMessageQueue; + } + + internal void ShutdownInternal() + { + if (NetworkLog.CurrentLogLevel <= LogLevel.Developer) + { + NetworkLog.LogInfo(nameof(ShutdownInternal)); + } + if (IsServer) { // make sure all messages are flushed before transport disconnect clients @@ -1075,11 +1096,15 @@ public void Shutdown() MessagingSystem = null; } - NetworkConfig.NetworkTransport.OnTransportEvent -= HandleRawTransportPoll; + if (NetworkConfig?.NetworkTransport != null) + { + NetworkConfig.NetworkTransport.OnTransportEvent -= HandleRawTransportPoll; + } if (SpawnManager != null) { - SpawnManager.DestroyNonSceneObjects(); + SpawnManager.CleanupAllTriggers(); + SpawnManager.DespawnAndDestroyNetworkObjects(); SpawnManager.ServerResetShudownStateForSceneObjects(); SpawnManager = null; @@ -1114,6 +1139,8 @@ public void Shutdown() m_TransportIdToClientIdMap.Clear(); IsListening = false; + m_ShuttingDown = false; + m_StopProcessingMessages = false; } // INetworkUpdateSystem @@ -1167,6 +1194,11 @@ private void OnNetworkPreUpdate() return; } + if (m_ShuttingDown && m_StopProcessingMessages) + { + return; + } + // Only update RTT here, server time is updated by time sync messages var reset = NetworkTimeSystem.Advance(Time.deltaTime); if (reset) @@ -1183,9 +1215,18 @@ private void OnNetworkPreUpdate() private void OnNetworkPostLateUpdate() { - MessagingSystem.ProcessSendQueues(); - NetworkMetrics.DispatchFrame(); + + if (!m_ShuttingDown || !m_StopProcessingMessages) + { + MessagingSystem.ProcessSendQueues(); + NetworkMetrics.DispatchFrame(); + } SpawnManager.CleanupStaleTriggers(); + + if (m_ShuttingDown) + { + ShutdownInternal(); + } } /// @@ -1312,6 +1353,8 @@ private void HandleRawTransportPoll(NetworkEvent networkEvent, ulong clientId, A #endif clientId = TransportIdToClientId(clientId); + OnClientDisconnectCallback?.Invoke(clientId); + m_TransportIdToClientIdMap.Remove(transportId); m_ClientIdToTransportIdMap.Remove(clientId); @@ -1328,9 +1371,6 @@ private void HandleRawTransportPoll(NetworkEvent networkEvent, ulong clientId, A { Shutdown(); } - - OnClientDisconnectCallback?.Invoke(clientId); - #if DEVELOPMENT_BUILD || UNITY_EDITOR s_TransportDisconnect.End(); #endif @@ -1596,6 +1636,7 @@ internal void HandleApproval(ulong ownerClientId, bool createPlayerObject, uint? } else // Server just adds itself as an observer to all spawned NetworkObjects { + LocalClient = client; SpawnManager.UpdateObservedNetworkObjects(ownerClientId); InvokeOnClientConnectedCallback(ownerClientId); } diff --git a/Runtime/Core/SnapshotRTT.cs b/Runtime/Core/SnapshotRtt.cs similarity index 100% rename from Runtime/Core/SnapshotRTT.cs rename to Runtime/Core/SnapshotRtt.cs diff --git a/Runtime/Core/SnapshotRTT.cs.meta b/Runtime/Core/SnapshotRtt.cs.meta similarity index 100% rename from Runtime/Core/SnapshotRTT.cs.meta rename to Runtime/Core/SnapshotRtt.cs.meta diff --git a/Runtime/Core/SnapshotSystem.cs b/Runtime/Core/SnapshotSystem.cs index b8fe48e..8d9a9c3 100644 --- a/Runtime/Core/SnapshotSystem.cs +++ b/Runtime/Core/SnapshotSystem.cs @@ -62,20 +62,63 @@ internal struct SnapshotSpawnCommand internal int TimesWritten; } + internal class ClientData + { + internal struct SentSpawn // this struct also stores Despawns, not just Spawns + { + internal ulong SequenceNumber; + internal ulong ObjectId; + internal int Tick; + } + + internal ushort SequenceNumber = 0; // the next sequence number to use for this client + internal ushort LastReceivedSequence = 0; // the last sequence number received by this client + internal ushort ReceivedSequenceMask = 0; // bitmask of the messages before the last one that we received. + + internal int NextSpawnIndex = 0; // index of the last spawn sent. Used to cycle through spawns (LRU scheme) + internal int NextDespawnIndex = 0; // same as above, but for despawns. + + // by objectId + // which spawns and despawns did this connection ack'ed ? + internal Dictionary SpawnAck = new Dictionary(); + + // list of spawn and despawns commands we sent, with sequence number + // need to manage acknowledgements + internal List SentSpawns = new List(); + } + + internal delegate int MockSendMessage(in SnapshotDataMessage message, NetworkDelivery delivery, ulong clientId); + internal delegate int MockSpawnObject(SnapshotSpawnCommand spawnCommand); + internal delegate int MockDespawnObject(SnapshotDespawnCommand despawnCommand); + + // A table of NetworkVariables that constitutes a Snapshot. // Stores serialized NetworkVariables // todo --M1-- // The Snapshot will change for M1b with memory management, instead of just FreeMemoryPosition, there will be data structure // around available buffer, etc. - internal class Snapshot + internal class SnapshotSystem : INetworkUpdateSystem, IDisposable { // todo --M1-- functionality to grow these will be needed in a later milestone private const int k_MaxVariables = 2000; - private int m_MaxSpawns = 100; - private int m_MaxDespawns = 100; + internal int SpawnsBufferCount { get; private set; } = 100; + internal int DespawnsBufferCount { get; private set; } = 100; private const int k_BufferSize = 30000; + private NetworkManager m_NetworkManager = default; + + // by clientId + private Dictionary m_ClientData = new Dictionary(); + private Dictionary m_ConnectionRtts = new Dictionary(); + + private bool m_UseSnapshotDelta; + private bool m_UseSnapshotSpawn; + private int m_SnapshotMaxSpawnUsage; + private NetworkTickSystem m_NetworkTickSystem; + + private int m_CurrentTick = NetworkTickSystem.NoTick; + internal byte[] MainBuffer = new byte[k_BufferSize]; // buffer holding a snapshot in memory internal byte[] RecvBuffer = new byte[k_BufferSize]; // buffer holding the received snapshot message @@ -90,23 +133,17 @@ internal class Snapshot internal SnapshotDespawnCommand[] Despawns; internal int NumDespawns = 0; - internal NetworkManager NetworkManager; - // indexed by ObjectId internal Dictionary TickAppliedSpawn = new Dictionary(); internal Dictionary TickAppliedDespawn = new Dictionary(); - /// - /// Constructor - /// Allocated a MemoryStream to be reused for this Snapshot - /// - internal Snapshot() - { - // we ask for twice as many slots because there could end up being one free spot between each pair of slot used - Allocator = new IndexAllocator(k_BufferSize, k_MaxVariables * 2); - Spawns = new SnapshotSpawnCommand[m_MaxSpawns]; - Despawns = new SnapshotDespawnCommand[m_MaxDespawns]; - } + internal bool IsServer { get; set; } + internal bool IsConnectedClient { get; set; } + internal ulong ServerClientId { get; set; } + internal List ConnectedClientsId { get; } = new List(); + internal MockSendMessage MockSendMessage { get; set; } + internal MockSpawnObject MockSpawnObject { get; set; } + internal MockDespawnObject MockDespawnObject { get; set; } internal void Clear() { @@ -156,15 +193,15 @@ internal List GetClientList() List clientList; clientList = new List(); - if (!NetworkManager.IsServer) + if (!IsServer) { - clientList.Add(NetworkManager.ServerClientId); + clientList.Add(m_NetworkManager.ServerClientId); } else { - foreach (var clientId in NetworkManager.ConnectedClientsIds) + foreach (var clientId in ConnectedClientsId) { - if (clientId != NetworkManager.ServerClientId) + if (clientId != m_NetworkManager.ServerClientId) { clientList.Add(clientId); } @@ -176,14 +213,14 @@ internal List GetClientList() internal void AddSpawn(SnapshotSpawnCommand command) { - if (NumSpawns >= m_MaxSpawns) + if (NumSpawns >= SpawnsBufferCount) { - Array.Resize(ref Spawns, 2 * m_MaxSpawns); - m_MaxSpawns = m_MaxSpawns * 2; + Array.Resize(ref Spawns, 2 * SpawnsBufferCount); + SpawnsBufferCount = SpawnsBufferCount * 2; // Debug.Log($"[JEFF] spawn size is now {m_MaxSpawns}"); } - if (NumSpawns < m_MaxSpawns) + if (NumSpawns < SpawnsBufferCount) { if (command.TargetClientIds == default) { @@ -208,14 +245,14 @@ internal void AddSpawn(SnapshotSpawnCommand command) internal void AddDespawn(SnapshotDespawnCommand command) { - if (NumDespawns >= m_MaxDespawns) + if (NumDespawns >= DespawnsBufferCount) { - Array.Resize(ref Despawns, 2 * m_MaxDespawns); - m_MaxDespawns = m_MaxDespawns * 2; + Array.Resize(ref Despawns, 2 * DespawnsBufferCount); + DespawnsBufferCount = DespawnsBufferCount * 2; // Debug.Log($"[JEFF] despawn size is now {m_MaxDespawns}"); } - if (NumDespawns < m_MaxDespawns) + if (NumDespawns < DespawnsBufferCount) { if (command.TargetClientIds == default) { @@ -229,6 +266,17 @@ internal void AddDespawn(SnapshotDespawnCommand command) } } + internal void ReduceBufferUsage() + { + var count = Math.Max(1, NumDespawns); + Array.Resize(ref Despawns, count); + DespawnsBufferCount = count; + + count = Math.Max(1, NumSpawns); + Array.Resize(ref Spawns, count); + SpawnsBufferCount = count; + } + internal ClientData.SentSpawn GetSpawnData(in ClientData clientData, in SnapshotSpawnCommand spawn, out SnapshotDataMessage.SpawnData data) { // remember which spawn we sent this connection with which sequence number @@ -417,7 +465,54 @@ internal void ReadIndex(in SnapshotDataMessage message) } } - internal void ReadSpawns(in SnapshotDataMessage message) + internal void SpawnObject(SnapshotSpawnCommand spawnCommand, ulong srcClientId) + { + if (m_NetworkManager) + { + NetworkObject networkObject; + if (spawnCommand.ParentNetworkId == spawnCommand.NetworkObjectId) + { + networkObject = m_NetworkManager.SpawnManager.CreateLocalNetworkObject(false, + spawnCommand.GlobalObjectIdHash, spawnCommand.OwnerClientId, null, spawnCommand.ObjectPosition, + spawnCommand.ObjectRotation); + } + else + { + networkObject = m_NetworkManager.SpawnManager.CreateLocalNetworkObject(false, + spawnCommand.GlobalObjectIdHash, spawnCommand.OwnerClientId, spawnCommand.ParentNetworkId, spawnCommand.ObjectPosition, + spawnCommand.ObjectRotation); + } + + m_NetworkManager.SpawnManager.SpawnNetworkObjectLocally(networkObject, spawnCommand.NetworkObjectId, + true, spawnCommand.IsPlayerObject, spawnCommand.OwnerClientId, false); + //todo: discuss with tools how to report shared bytes + m_NetworkManager.NetworkMetrics.TrackObjectSpawnReceived(srcClientId, networkObject, 8); + } + else + { + MockSpawnObject(spawnCommand); + } + } + + internal void DespawnObject(SnapshotDespawnCommand despawnCommand, ulong srcClientId) + { + if (m_NetworkManager) + { + m_NetworkManager.SpawnManager.SpawnedObjects.TryGetValue(despawnCommand.NetworkObjectId, + out NetworkObject networkObject); + + m_NetworkManager.SpawnManager.OnDespawnObject(networkObject, true); + //todo: discuss with tools how to report shared bytes + m_NetworkManager.NetworkMetrics.TrackObjectDestroyReceived(srcClientId, networkObject, 8); + } + else + { + MockDespawnObject(despawnCommand); + } + } + + + internal void ReadSpawns(in SnapshotDataMessage message, ulong srcClientId) { SnapshotSpawnCommand spawnCommand; SnapshotDespawnCommand despawnCommand; @@ -436,16 +531,7 @@ internal void ReadSpawns(in SnapshotDataMessage message) // Debug.Log($"[Spawn] {spawnCommand.NetworkObjectId} {spawnCommand.TickWritten}"); - if (spawnCommand.ParentNetworkId == spawnCommand.NetworkObjectId) - { - var networkObject = NetworkManager.SpawnManager.CreateLocalNetworkObject(false, spawnCommand.GlobalObjectIdHash, spawnCommand.OwnerClientId, null, spawnCommand.ObjectPosition, spawnCommand.ObjectRotation); - NetworkManager.SpawnManager.SpawnNetworkObjectLocally(networkObject, spawnCommand.NetworkObjectId, true, spawnCommand.IsPlayerObject, spawnCommand.OwnerClientId, false); - } - else - { - var networkObject = NetworkManager.SpawnManager.CreateLocalNetworkObject(false, spawnCommand.GlobalObjectIdHash, spawnCommand.OwnerClientId, spawnCommand.ParentNetworkId, spawnCommand.ObjectPosition, spawnCommand.ObjectRotation); - NetworkManager.SpawnManager.SpawnNetworkObjectLocally(networkObject, spawnCommand.NetworkObjectId, true, spawnCommand.IsPlayerObject, spawnCommand.OwnerClientId, false); - } + SpawnObject(spawnCommand, srcClientId); } for (var i = 0; i < message.Despawns.Length; i++) { @@ -461,10 +547,7 @@ internal void ReadSpawns(in SnapshotDataMessage message) // Debug.Log($"[DeSpawn] {despawnCommand.NetworkObjectId} {despawnCommand.TickWritten}"); - NetworkManager.SpawnManager.SpawnedObjects.TryGetValue(despawnCommand.NetworkObjectId, - out NetworkObject networkObject); - - NetworkManager.SpawnManager.OnDespawnObject(networkObject, true); + DespawnObject(despawnCommand, srcClientId); } } @@ -569,7 +652,7 @@ internal void ProcessSingleAck(ushort ackSequence, ulong clientId, ClientData cl /// The key to search for private NetworkVariableBase FindNetworkVar(VariableKey key) { - var spawnedObjects = NetworkManager.SpawnManager.SpawnedObjects; + var spawnedObjects = m_NetworkManager.SpawnManager.SpawnedObjects; if (spawnedObjects.ContainsKey(key.NetworkObjectId)) { @@ -580,61 +663,49 @@ private NetworkVariableBase FindNetworkVar(VariableKey key) return null; } - } - - - internal class ClientData - { - internal struct SentSpawn // this struct also stores Despawns, not just Spawns - { - internal ulong SequenceNumber; - internal ulong ObjectId; - internal int Tick; - } - - internal ushort SequenceNumber = 0; // the next sequence number to use for this client - internal ushort LastReceivedSequence = 0; // the last sequence number received by this client - internal ushort ReceivedSequenceMask = 0; // bitmask of the messages before the last one that we received. - - internal int NextSpawnIndex = 0; // index of the last spawn sent. Used to cycle through spawns (LRU scheme) - internal int NextDespawnIndex = 0; // same as above, but for despawns. - - // by objectId - // which spawns and despawns did this connection ack'ed ? - internal Dictionary SpawnAck = new Dictionary(); - - // list of spawn and despawns commands we sent, with sequence number - // need to manage acknowledgements - internal List SentSpawns = new List(); - } - - internal class SnapshotSystem : INetworkUpdateSystem, IDisposable - { - // temporary, debugging sentinels - internal const ushort SentinelBefore = 0x4246; - internal const ushort SentinelAfter = 0x89CE; - - private NetworkManager m_NetworkManager = default; - private Snapshot m_Snapshot = default; - - // by clientId - private Dictionary m_ClientData = new Dictionary(); - private Dictionary m_ConnectionRtts = new Dictionary(); - - private int m_CurrentTick = NetworkTickSystem.NoTick; /// /// Constructor /// /// Registers the snapshot system for early updates, keeps reference to the NetworkManager - internal SnapshotSystem(NetworkManager networkManager) + internal SnapshotSystem(NetworkManager networkManager, NetworkConfig config, NetworkTickSystem networkTickSystem) { - m_Snapshot = new Snapshot(); - m_NetworkManager = networkManager; - m_Snapshot.NetworkManager = networkManager; + m_NetworkTickSystem = networkTickSystem; + + m_UseSnapshotDelta = config.UseSnapshotDelta; + m_UseSnapshotSpawn = config.UseSnapshotSpawn; + m_SnapshotMaxSpawnUsage = config.SnapshotMaxSpawnUsage; + + UpdateClientServerData(); this.RegisterNetworkUpdate(NetworkUpdateStage.EarlyUpdate); + + // we ask for twice as many slots because there could end up being one free spot between each pair of slot used + Allocator = new IndexAllocator(k_BufferSize, k_MaxVariables * 2); + Spawns = new SnapshotSpawnCommand[SpawnsBufferCount]; + Despawns = new SnapshotDespawnCommand[DespawnsBufferCount]; + } + + // since we don't want to access the NetworkManager directly, we refresh those values on Update + internal void UpdateClientServerData() + { + if (m_NetworkManager) + { + IsServer = m_NetworkManager.IsServer; + IsConnectedClient = m_NetworkManager.IsConnectedClient; + ServerClientId = m_NetworkManager.ServerClientId; + + // todo: This is extremely inefficient. What is the efficient and idiomatic way ? + ConnectedClientsId.Clear(); + if (IsServer) + { + foreach (var id in m_NetworkManager.ConnectedClientsIds) + { + ConnectedClientsId.Add(id); + } + } + } } internal ConnectionRtt GetConnectionRtt(ulong clientId) @@ -658,34 +729,36 @@ public void Dispose() public void NetworkUpdate(NetworkUpdateStage updateStage) { - if (!m_NetworkManager.NetworkConfig.UseSnapshotDelta && !m_NetworkManager.NetworkConfig.UseSnapshotSpawn) + if (!m_UseSnapshotDelta && !m_UseSnapshotSpawn) { return; } if (updateStage == NetworkUpdateStage.EarlyUpdate) { - var tick = m_NetworkManager.NetworkTickSystem.LocalTime.Tick; + UpdateClientServerData(); + + var tick = m_NetworkTickSystem.LocalTime.Tick; if (tick != m_CurrentTick) { m_CurrentTick = tick; - if (m_NetworkManager.IsServer) + if (IsServer) { - for (int i = 0; i < m_NetworkManager.ConnectedClientsList.Count; i++) + for (int i = 0; i < ConnectedClientsId.Count; i++) { - var clientId = m_NetworkManager.ConnectedClientsList[i].ClientId; + var clientId = ConnectedClientsId[i]; // don't send to ourselves - if (clientId != m_NetworkManager.ServerClientId) + if (clientId != ServerClientId) { SendSnapshot(clientId); } } } - else if (m_NetworkManager.IsConnectedClient) + else if (IsConnectedClient) { - SendSnapshot(m_NetworkManager.ServerClientId); + SendSnapshot(ServerClientId); } } @@ -721,12 +794,12 @@ private void SendSnapshot(ulong clientId) { CurrentTick = m_CurrentTick, Sequence = sequence, - Range = (ushort)m_Snapshot.Allocator.Range, + Range = (ushort)Allocator.Range, // todo --M1-- // this sends the whole buffer // we'll need to build a per-client list - SendMainBuffer = m_Snapshot.MainBuffer, + SendMainBuffer = MainBuffer, Ack = new SnapshotDataMessage.AckData { @@ -740,7 +813,14 @@ private void SendSnapshot(ulong clientId) WriteIndex(ref message); WriteSpawns(ref message, clientId); - m_NetworkManager.SendMessage(message, NetworkDelivery.Unreliable, clientId); + if (m_NetworkManager) + { + m_NetworkManager.SendMessage(message, NetworkDelivery.Unreliable, clientId); + } + else + { + MockSendMessage(message, NetworkDelivery.Unreliable, clientId); + } m_ClientData[clientId].LastReceivedSequence = 0; @@ -791,51 +871,51 @@ private void WriteSpawns(ref SnapshotDataMessage message, ulong clientId) ClientData clientData = m_ClientData[clientId]; // this is needed because spawns being removed may have reduce the size below LRU position - if (m_Snapshot.NumSpawns > 0) + if (NumSpawns > 0) { - clientData.NextSpawnIndex %= m_Snapshot.NumSpawns; + clientData.NextSpawnIndex %= NumSpawns; } else { clientData.NextSpawnIndex = 0; } - if (m_Snapshot.NumDespawns > 0) + if (NumDespawns > 0) { - clientData.NextDespawnIndex %= m_Snapshot.NumDespawns; + clientData.NextDespawnIndex %= NumDespawns; } else { clientData.NextDespawnIndex = 0; } - message.Spawns = new NativeList(m_Snapshot.NumSpawns, Allocator.TempJob); - message.Despawns = new NativeList(m_Snapshot.NumDespawns, Allocator.TempJob); + message.Spawns = new NativeList(NumSpawns, Collections.Allocator.TempJob); + message.Despawns = new NativeList(NumDespawns, Collections.Allocator.TempJob); var spawnUsage = 0; - for (var j = 0; j < m_Snapshot.NumSpawns && !overSize; j++) + for (var j = 0; j < NumSpawns && !overSize; j++) { var index = clientData.NextSpawnIndex; // todo: re-enable ShouldWriteSpawn, once we have a mechanism to not let despawn pass in front of spawns - if (m_Snapshot.Spawns[index].TargetClientIds.Contains(clientId) /*&& ShouldWriteSpawn(m_Snapshot.Spawns[index])*/) + if (Spawns[index].TargetClientIds.Contains(clientId) /*&& ShouldWriteSpawn(Spawns[index])*/) { spawnUsage += FastBufferWriter.GetWriteSize(); // limit spawn sizes, compare current pos to very first position we wrote to - if (spawnUsage > m_NetworkManager.NetworkConfig.SnapshotMaxSpawnUsage) + if (spawnUsage > m_SnapshotMaxSpawnUsage) { overSize = true; break; } - var sentSpawn = m_Snapshot.GetSpawnData(clientData, in m_Snapshot.Spawns[index], out var spawn); + var sentSpawn = GetSpawnData(clientData, in Spawns[index], out var spawn); message.Spawns.Add(spawn); - m_Snapshot.Spawns[index].TimesWritten++; + Spawns[index].TimesWritten++; clientData.SentSpawns.Add(sentSpawn); spawnWritten++; } - clientData.NextSpawnIndex = (clientData.NextSpawnIndex + 1) % m_Snapshot.NumSpawns; + clientData.NextSpawnIndex = (clientData.NextSpawnIndex + 1) % NumSpawns; } // even though we might have a spawn we could not fit, it's possible despawns will fit (they're smaller) @@ -846,28 +926,28 @@ private void WriteSpawns(ref SnapshotDataMessage message, ulong clientId) // As-is it is overly restrictive but allows us to go forward without the spawn/despawn dependency check // overSize = false; - for (var j = 0; j < m_Snapshot.NumDespawns && !overSize; j++) + for (var j = 0; j < NumDespawns && !overSize; j++) { var index = clientData.NextDespawnIndex; // todo: re-enable ShouldWriteSpawn, once we have a mechanism to not let despawn pass in front of spawns - if (m_Snapshot.Despawns[index].TargetClientIds.Contains(clientId) /*&& ShouldWriteDespawn(m_Snapshot.Despawns[index])*/) + if (Despawns[index].TargetClientIds.Contains(clientId) /*&& ShouldWriteDespawn(Despawns[index])*/) { spawnUsage += FastBufferWriter.GetWriteSize(); // limit spawn sizes, compare current pos to very first position we wrote to - if (spawnUsage > m_NetworkManager.NetworkConfig.SnapshotMaxSpawnUsage) + if (spawnUsage > m_SnapshotMaxSpawnUsage) { overSize = true; break; } - var sentDespawn = m_Snapshot.GetDespawnData(clientData, in m_Snapshot.Despawns[index], out var despawn); + var sentDespawn = GetDespawnData(clientData, in Despawns[index], out var despawn); message.Despawns.Add(despawn); - m_Snapshot.Despawns[index].TimesWritten++; + Despawns[index].TimesWritten++; clientData.SentSpawns.Add(sentDespawn); despawnWritten++; } - clientData.NextDespawnIndex = (clientData.NextDespawnIndex + 1) % m_Snapshot.NumDespawns; + clientData.NextDespawnIndex = (clientData.NextDespawnIndex + 1) % NumDespawns; } } @@ -877,10 +957,10 @@ private void WriteSpawns(ref SnapshotDataMessage message, ulong clientId) /// The message to write the index to private void WriteIndex(ref SnapshotDataMessage message) { - message.Entries = new NativeList(m_Snapshot.LastEntry, Allocator.TempJob); - for (var i = 0; i < m_Snapshot.LastEntry; i++) + message.Entries = new NativeList(LastEntry, Collections.Allocator.TempJob); + for (var i = 0; i < LastEntry; i++) { - var entryMeta = m_Snapshot.Entries[i]; + var entryMeta = Entries[i]; var entry = entryMeta.Key; message.Entries.Add(new SnapshotDataMessage.EntryData { @@ -897,7 +977,7 @@ private void WriteIndex(ref SnapshotDataMessage message) internal void Spawn(SnapshotSpawnCommand command) { command.TickWritten = m_CurrentTick; - m_Snapshot.AddSpawn(command); + AddSpawn(command); // Debug.Log($"[Spawn] {command.NetworkObjectId} {command.TickWritten}"); } @@ -905,7 +985,7 @@ internal void Spawn(SnapshotSpawnCommand command) internal void Despawn(SnapshotDespawnCommand command) { command.TickWritten = m_CurrentTick; - m_Snapshot.AddDespawn(command); + AddDespawn(command); // Debug.Log($"[DeSpawn] {command.NetworkObjectId} {command.TickWritten}"); } @@ -922,35 +1002,35 @@ internal void Store(ulong networkObjectId, int behaviourIndex, int variableIndex k.NetworkObjectId = networkObjectId; k.BehaviourIndex = (ushort)behaviourIndex; k.VariableIndex = (ushort)variableIndex; - k.TickWritten = m_NetworkManager.NetworkTickSystem.LocalTime.Tick; + k.TickWritten = m_NetworkTickSystem.LocalTime.Tick; - int pos = m_Snapshot.Find(k); + int pos = Find(k); if (pos == Entry.NotFound) { - pos = m_Snapshot.AddEntry(k); + pos = AddEntry(k); } - m_Snapshot.Entries[pos].Key.TickWritten = k.TickWritten; + Entries[pos].Key.TickWritten = k.TickWritten; - WriteVariableToSnapshot(m_Snapshot, networkVariable, pos); + WriteVariable(networkVariable, pos); } - private unsafe void WriteVariableToSnapshot(Snapshot snapshot, NetworkVariableBase networkVariable, int index) + private unsafe void WriteVariable(NetworkVariableBase networkVariable, int index) { // write var into buffer, possibly adjusting entry's position and Length - var varBuffer = new FastBufferWriter(MessagingSystem.NON_FRAGMENTED_MESSAGE_MAX_SIZE, Allocator.Temp); + var varBuffer = new FastBufferWriter(MessagingSystem.NON_FRAGMENTED_MESSAGE_MAX_SIZE, Collections.Allocator.Temp); using (varBuffer) { networkVariable.WriteDelta(varBuffer); - if (varBuffer.Length > snapshot.Entries[index].Length) + if (varBuffer.Length > Entries[index].Length) { // allocate this Entry's buffer - snapshot.AllocateEntry(ref snapshot.Entries[index], index, (int)varBuffer.Length); + AllocateEntry(ref Entries[index], index, (int)varBuffer.Length); } - fixed (byte* buffer = snapshot.MainBuffer) + fixed (byte* buffer = MainBuffer) { - UnsafeUtility.MemCpy(buffer + snapshot.Entries[index].Position, varBuffer.GetUnsafePtr(), varBuffer.Length); + UnsafeUtility.MemCpy(buffer + Entries[index].Position, varBuffer.GetUnsafePtr(), varBuffer.Length); } } } @@ -1009,10 +1089,10 @@ internal void HandleSnapshot(ulong clientId, in SnapshotDataMessage message) // without this, we incur extra retransmit, not a catastrophic failure } - m_Snapshot.ReadBuffer(message); - m_Snapshot.ReadIndex(message); - m_Snapshot.ReadAcks(clientId, m_ClientData[clientId], message, GetConnectionRtt(clientId)); - m_Snapshot.ReadSpawns(message); + ReadBuffer(message); + ReadIndex(message); + ReadAcks(clientId, m_ClientData[clientId], message, GetConnectionRtt(clientId)); + ReadSpawns(message, clientId); } // todo --M1-- @@ -1024,14 +1104,14 @@ private void DebugDisplayStore() table += $"We're clientId {m_NetworkManager.LocalClientId}\n"; table += "=== Variables ===\n"; - for (int i = 0; i < m_Snapshot.LastEntry; i++) + for (int i = 0; i < LastEntry; i++) { - table += string.Format("NetworkVariable {0}:{1}:{2} written {5}, range [{3}, {4}] ", m_Snapshot.Entries[i].Key.NetworkObjectId, m_Snapshot.Entries[i].Key.BehaviourIndex, - m_Snapshot.Entries[i].Key.VariableIndex, m_Snapshot.Entries[i].Position, m_Snapshot.Entries[i].Position + m_Snapshot.Entries[i].Length, m_Snapshot.Entries[i].Key.TickWritten); + table += string.Format("NetworkVariable {0}:{1}:{2} written {5}, range [{3}, {4}] ", Entries[i].Key.NetworkObjectId, Entries[i].Key.BehaviourIndex, + Entries[i].Key.VariableIndex, Entries[i].Position, Entries[i].Position + Entries[i].Length, Entries[i].Key.TickWritten); - for (int j = 0; j < m_Snapshot.Entries[i].Length && j < 4; j++) + for (int j = 0; j < Entries[i].Length && j < 4; j++) { - table += m_Snapshot.MainBuffer[m_Snapshot.Entries[i].Position + j].ToString("X2") + " "; + table += MainBuffer[Entries[i].Position + j].ToString("X2") + " "; } table += "\n"; @@ -1039,14 +1119,14 @@ private void DebugDisplayStore() table += "=== Spawns ===\n"; - for (int i = 0; i < m_Snapshot.NumSpawns; i++) + for (int i = 0; i < NumSpawns; i++) { string targets = ""; - foreach (var target in m_Snapshot.Spawns[i].TargetClientIds) + foreach (var target in Spawns[i].TargetClientIds) { targets += target.ToString() + ", "; } - table += $"Spawn Object Id {m_Snapshot.Spawns[i].NetworkObjectId}, Tick {m_Snapshot.Spawns[i].TickWritten}, Target {targets}\n"; + table += $"Spawn Object Id {Spawns[i].NetworkObjectId}, Tick {Spawns[i].TickWritten}, Target {targets}\n"; } table += $"=== RTTs ===\n"; diff --git a/Runtime/Messaging/CustomMessageManager.cs b/Runtime/Messaging/CustomMessageManager.cs index 0b4e9a0..7a7938f 100644 --- a/Runtime/Messaging/CustomMessageManager.cs +++ b/Runtime/Messaging/CustomMessageManager.cs @@ -28,7 +28,7 @@ internal CustomMessagingManager(NetworkManager networkManager) /// public event UnnamedMessageDelegate OnUnnamedMessage; - internal void InvokeUnnamedMessage(ulong clientId, FastBufferReader reader) + internal void InvokeUnnamedMessage(ulong clientId, FastBufferReader reader, int serializedHeaderSize) { if (OnUnnamedMessage != null) { @@ -40,7 +40,7 @@ internal void InvokeUnnamedMessage(ulong clientId, FastBufferReader reader) ((UnnamedMessageDelegate)handler).Invoke(clientId, reader); } } - m_NetworkManager.NetworkMetrics.TrackUnnamedMessageReceived(clientId, reader.Length + FastBufferWriter.GetWriteSize()); + m_NetworkManager.NetworkMetrics.TrackUnnamedMessageReceived(clientId, reader.Length + serializedHeaderSize); } /// @@ -115,9 +115,9 @@ public void SendUnnamedMessage(ulong clientId, FastBufferWriter messageBuffer, N private Dictionary m_MessageHandlerNameLookup32 = new Dictionary(); private Dictionary m_MessageHandlerNameLookup64 = new Dictionary(); - internal void InvokeNamedMessage(ulong hash, ulong sender, FastBufferReader reader) + internal void InvokeNamedMessage(ulong hash, ulong sender, FastBufferReader reader, int serializedHeaderSize) { - var bytesCount = reader.Length + FastBufferWriter.GetWriteSize(); + var bytesCount = reader.Length + serializedHeaderSize; if (m_NetworkManager == null) { diff --git a/Runtime/Messaging/MessageHeader.cs b/Runtime/Messaging/MessageHeader.cs index a3a9543..a7e3ef1 100644 --- a/Runtime/Messaging/MessageHeader.cs +++ b/Runtime/Messaging/MessageHeader.cs @@ -11,11 +11,12 @@ internal struct MessageHeader /// unchanged - if new messages are added or messages are removed, MessageType assignments may be /// calculated differently. /// - public byte MessageType; + public uint MessageType; /// /// The total size of the message, NOT including the header. + /// Stored as a uint to avoid zig-zag encoding, but capped at int.MaxValue. /// - public ushort MessageSize; + public uint MessageSize; } } diff --git a/Runtime/Messaging/Messages/NamedMessage.cs b/Runtime/Messaging/Messages/NamedMessage.cs index a6bab60..2583d4f 100644 --- a/Runtime/Messaging/Messages/NamedMessage.cs +++ b/Runtime/Messaging/Messages/NamedMessage.cs @@ -16,7 +16,7 @@ public static void Receive(FastBufferReader reader, in NetworkContext context) var message = new NamedMessage(); reader.ReadValueSafe(out message.Hash); - ((NetworkManager)context.SystemOwner).CustomMessagingManager.InvokeNamedMessage(message.Hash, context.SenderId, reader); + ((NetworkManager)context.SystemOwner).CustomMessagingManager.InvokeNamedMessage(message.Hash, context.SenderId, reader, context.SerializedHeaderSize); } } } diff --git a/Runtime/Messaging/Messages/SnapshotDataMessage.cs b/Runtime/Messaging/Messages/SnapshotDataMessage.cs index 1d1ec8b..bc0aa94 100644 --- a/Runtime/Messaging/Messages/SnapshotDataMessage.cs +++ b/Runtime/Messaging/Messages/SnapshotDataMessage.cs @@ -104,7 +104,6 @@ public unsafe void Serialize(FastBufferWriter writer) public static unsafe void Receive(FastBufferReader reader, in NetworkContext context) { - var networkManager = (NetworkManager)context.SystemOwner; var message = new SnapshotDataMessage(); if (!reader.TryBeginRead( FastBufferWriter.GetWriteSize(message.CurrentTick) + @@ -142,20 +141,32 @@ public static unsafe void Receive(FastBufferReader reader, in NetworkContext con using (message.Spawns) using (message.Despawns) { - message.Handle(context.SenderId, networkManager); + message.Handle(context.SenderId, context.SystemOwner); } } - public void Handle(ulong senderId, NetworkManager networkManager) + public void Handle(ulong senderId, object systemOwner) { - // todo: temporary hack around bug - if (!networkManager.IsServer) + if (systemOwner is NetworkManager) { - senderId = networkManager.ServerClientId; - } + var networkManager = (NetworkManager)systemOwner; + + // todo: temporary hack around bug + if (!networkManager.IsServer) + { + senderId = networkManager.ServerClientId; + } - var snapshotSystem = networkManager.SnapshotSystem; - snapshotSystem.HandleSnapshot(senderId, this); + var snapshotSystem = networkManager.SnapshotSystem; + snapshotSystem.HandleSnapshot(senderId, this); + } + else + { + var ownerData = (Tuple)systemOwner; + var snapshotSystem = ownerData.Item1; + snapshotSystem.HandleSnapshot(ownerData.Item2, this); + return; + } } } } diff --git a/Runtime/Messaging/Messages/UnnamedMessage.cs b/Runtime/Messaging/Messages/UnnamedMessage.cs index 9f349de..7969e88 100644 --- a/Runtime/Messaging/Messages/UnnamedMessage.cs +++ b/Runtime/Messaging/Messages/UnnamedMessage.cs @@ -11,7 +11,7 @@ public unsafe void Serialize(FastBufferWriter writer) public static void Receive(FastBufferReader reader, in NetworkContext context) { - ((NetworkManager)context.SystemOwner).CustomMessagingManager.InvokeUnnamedMessage(context.SenderId, reader); + ((NetworkManager)context.SystemOwner).CustomMessagingManager.InvokeUnnamedMessage(context.SenderId, reader, context.SerializedHeaderSize); } } } diff --git a/Runtime/Messaging/MessagingSystem.cs b/Runtime/Messaging/MessagingSystem.cs index 41d27a6..a74a856 100644 --- a/Runtime/Messaging/MessagingSystem.cs +++ b/Runtime/Messaging/MessagingSystem.cs @@ -23,6 +23,7 @@ private struct ReceiveQueueItem public MessageHeader Header; public ulong SenderId; public float Timestamp; + public int MessageHeaderSerializedSize; } private struct SendQueueItem @@ -46,27 +47,27 @@ public SendQueueItem(NetworkDelivery delivery, int writerSize, Allocator writerA private MessageHandler[] m_MessageHandlers = new MessageHandler[255]; private Type[] m_ReverseTypeMap = new Type[255]; - private Dictionary m_MessageTypes = new Dictionary(); + private Dictionary m_MessageTypes = new Dictionary(); private Dictionary> m_SendQueues = new Dictionary>(); private List m_Hooks = new List(); - private byte m_HighMessageType; + private uint m_HighMessageType; private object m_Owner; private IMessageSender m_MessageSender; private bool m_Disposed; internal Type[] MessageTypes => m_ReverseTypeMap; internal MessageHandler[] MessageHandlers => m_MessageHandlers; - internal int MessageHandlerCount => m_HighMessageType; + internal uint MessageHandlerCount => m_HighMessageType; - internal byte GetMessageType(Type t) + internal uint GetMessageType(Type t) { return m_MessageTypes[t]; } public const int NON_FRAGMENTED_MESSAGE_MAX_SIZE = 1300; - public const int FRAGMENTED_MESSAGE_MAX_SIZE = 64000; + public const int FRAGMENTED_MESSAGE_MAX_SIZE = int.MaxValue; internal struct MessageWithHandler { @@ -100,7 +101,7 @@ public MessagingSystem(IMessageSender messageSender, object owner, IMessageProvi } } - public void Dispose() + public unsafe void Dispose() { if (m_Disposed) { @@ -113,6 +114,14 @@ public void Dispose() { CleanupDisconnectedClient(kvp.Key); } + + for (var queueIndex = 0; queueIndex < m_IncomingMessageQueue.Length; ++queueIndex) + { + // Avoid copies... + ref var item = ref m_IncomingMessageQueue.GetUnsafeList()->ElementAt(queueIndex); + item.Reader.Dispose(); + } + m_IncomingMessageQueue.Dispose(); m_Disposed = true; } @@ -141,7 +150,7 @@ internal void HandleIncomingData(ulong clientId, ArraySegment data, float fixed (byte* nativeData = data.Array) { var batchReader = - new FastBufferReader(nativeData, Allocator.None, data.Count, data.Offset); + new FastBufferReader(nativeData + data.Offset, Allocator.None, data.Count); if (!batchReader.TryBeginRead(sizeof(BatchHeader))) { NetworkLog.LogWarning("Received a packet too small to contain a BatchHeader. Ignoring it."); @@ -157,14 +166,23 @@ internal void HandleIncomingData(ulong clientId, ArraySegment data, float for (var messageIdx = 0; messageIdx < batchHeader.BatchSize; ++messageIdx) { - if (!batchReader.TryBeginRead(sizeof(MessageHeader))) + + var messageHeader = new MessageHeader(); + var position = batchReader.Position; + try + { + ByteUnpacker.ReadValueBitPacked(batchReader, out messageHeader.MessageType); + ByteUnpacker.ReadValueBitPacked(batchReader, out messageHeader.MessageSize); + } + catch (OverflowException) { NetworkLog.LogWarning("Received a batch that didn't have enough data for all of its batches, ending early!"); - return; + throw; } - batchReader.ReadValue(out MessageHeader messageHeader); - if (!batchReader.TryBeginRead(messageHeader.MessageSize)) + var receivedHeaderSize = batchReader.Position - position; + + if (!batchReader.TryBeginRead((int)messageHeader.MessageSize)) { NetworkLog.LogWarning("Received a message that claimed a size larger than the packet, ending early!"); return; @@ -177,9 +195,10 @@ internal void HandleIncomingData(ulong clientId, ArraySegment data, float // Copy the data for this message into a new FastBufferReader that owns that memory. // We can't guarantee the memory in the ArraySegment stays valid because we don't own it, // so we must move it to memory we do own. - Reader = new FastBufferReader(batchReader.GetUnsafePtrAtCurrentPosition(), Allocator.TempJob, messageHeader.MessageSize) + Reader = new FastBufferReader(batchReader.GetUnsafePtrAtCurrentPosition(), Allocator.TempJob, (int)messageHeader.MessageSize), + MessageHeaderSerializedSize = receivedHeaderSize, }); - batchReader.Seek(batchReader.Position + messageHeader.MessageSize); + batchReader.Seek(batchReader.Position + (int)messageHeader.MessageSize); } for (var hookIdx = 0; hookIdx < m_Hooks.Count; ++hookIdx) { @@ -202,7 +221,7 @@ private bool CanReceive(ulong clientId, Type messageType) return true; } - public void HandleMessage(in MessageHeader header, FastBufferReader reader, ulong senderId, float timestamp) + public void HandleMessage(in MessageHeader header, FastBufferReader reader, ulong senderId, float timestamp, int serializedHeaderSize) { if (header.MessageType >= m_HighMessageType) { @@ -215,8 +234,10 @@ public void HandleMessage(in MessageHeader header, FastBufferReader reader, ulon SystemOwner = m_Owner, SenderId = senderId, Timestamp = timestamp, - Header = header + Header = header, + SerializedHeaderSize = serializedHeaderSize, }; + var type = m_ReverseTypeMap[header.MessageType]; if (!CanReceive(senderId, type)) { @@ -228,6 +249,7 @@ public void HandleMessage(in MessageHeader header, FastBufferReader reader, ulon { m_Hooks[hookIdx].OnBeforeReceiveMessage(senderId, type, reader.Length + FastBufferWriter.GetWriteSize()); } + var handler = m_MessageHandlers[header.MessageType]; using (reader) { @@ -253,11 +275,15 @@ public void HandleMessage(in MessageHeader header, FastBufferReader reader, ulon internal unsafe void ProcessIncomingMessageQueue() { - for (var i = 0; i < m_IncomingMessageQueue.Length; ++i) + for (var index = 0; index < m_IncomingMessageQueue.Length; ++index) { // Avoid copies... - ref var item = ref m_IncomingMessageQueue.GetUnsafeList()->ElementAt(i); - HandleMessage(item.Header, item.Reader, item.SenderId, item.Timestamp); + ref var item = ref m_IncomingMessageQueue.GetUnsafeList()->ElementAt(index); + HandleMessage(item.Header, item.Reader, item.SenderId, item.Timestamp, item.MessageHeaderSerializedSize); + if (m_Disposed) + { + return; + } } m_IncomingMessageQueue.Clear(); @@ -316,64 +342,68 @@ internal unsafe int SendMessage(in TMessageType } var maxSize = delivery == NetworkDelivery.ReliableFragmentedSequenced ? FRAGMENTED_MESSAGE_MAX_SIZE : NON_FRAGMENTED_MESSAGE_MAX_SIZE; - var tmpSerializer = new FastBufferWriter(NON_FRAGMENTED_MESSAGE_MAX_SIZE - FastBufferWriter.GetWriteSize(), Allocator.Temp, maxSize - FastBufferWriter.GetWriteSize()); - using (tmpSerializer) + + using var tmpSerializer = new FastBufferWriter(NON_FRAGMENTED_MESSAGE_MAX_SIZE - FastBufferWriter.GetWriteSize(), Allocator.Temp, maxSize - FastBufferWriter.GetWriteSize()); + + message.Serialize(tmpSerializer); + + using var headerSerializer = new FastBufferWriter(FastBufferWriter.GetWriteSize(), Allocator.Temp); + + var header = new MessageHeader { - message.Serialize(tmpSerializer); + MessageSize = (ushort)tmpSerializer.Length, + MessageType = m_MessageTypes[typeof(TMessageType)], + }; + BytePacker.WriteValueBitPacked(headerSerializer, header.MessageType); + BytePacker.WriteValueBitPacked(headerSerializer, header.MessageSize); - for (var i = 0; i < clientIds.Count; ++i) - { - var clientId = clientIds[i]; + for (var i = 0; i < clientIds.Count; ++i) + { + var clientId = clientIds[i]; - if (!CanSend(clientId, typeof(TMessageType), delivery)) - { - continue; - } + if (!CanSend(clientId, typeof(TMessageType), delivery)) + { + continue; + } - for (var hookIdx = 0; hookIdx < m_Hooks.Count; ++hookIdx) - { - m_Hooks[hookIdx].OnBeforeSendMessage(clientId, typeof(TMessageType), delivery); - } + for (var hookIdx = 0; hookIdx < m_Hooks.Count; ++hookIdx) + { + m_Hooks[hookIdx].OnBeforeSendMessage(clientId, typeof(TMessageType), delivery); + } - var sendQueueItem = m_SendQueues[clientId]; - if (sendQueueItem.Length == 0) + var sendQueueItem = m_SendQueues[clientId]; + if (sendQueueItem.Length == 0) + { + sendQueueItem.Add(new SendQueueItem(delivery, NON_FRAGMENTED_MESSAGE_MAX_SIZE, Allocator.TempJob, + maxSize)); + sendQueueItem.GetUnsafeList()->ElementAt(0).Writer.Seek(sizeof(BatchHeader)); + } + else + { + ref var lastQueueItem = ref sendQueueItem.GetUnsafeList()->ElementAt(sendQueueItem.Length - 1); + if (lastQueueItem.NetworkDelivery != delivery || + lastQueueItem.Writer.MaxCapacity - lastQueueItem.Writer.Position + < tmpSerializer.Length + headerSerializer.Length) { sendQueueItem.Add(new SendQueueItem(delivery, NON_FRAGMENTED_MESSAGE_MAX_SIZE, Allocator.TempJob, maxSize)); - sendQueueItem.GetUnsafeList()->ElementAt(0).Writer.Seek(sizeof(BatchHeader)); - } - else - { - ref var lastQueueItem = ref sendQueueItem.GetUnsafeList()->ElementAt(sendQueueItem.Length - 1); - if (lastQueueItem.NetworkDelivery != delivery || - lastQueueItem.Writer.MaxCapacity - lastQueueItem.Writer.Position - < tmpSerializer.Length + FastBufferWriter.GetWriteSize()) - { - sendQueueItem.Add(new SendQueueItem(delivery, NON_FRAGMENTED_MESSAGE_MAX_SIZE, Allocator.TempJob, - maxSize)); - sendQueueItem.GetUnsafeList()->ElementAt(sendQueueItem.Length - 1).Writer.Seek(sizeof(BatchHeader)); - } + sendQueueItem.GetUnsafeList()->ElementAt(sendQueueItem.Length - 1).Writer.Seek(sizeof(BatchHeader)); } + } - ref var writeQueueItem = ref sendQueueItem.GetUnsafeList()->ElementAt(sendQueueItem.Length - 1); - writeQueueItem.Writer.TryBeginWrite(tmpSerializer.Length + FastBufferWriter.GetWriteSize()); - var header = new MessageHeader - { - MessageSize = (ushort)tmpSerializer.Length, - MessageType = m_MessageTypes[typeof(TMessageType)], - }; + ref var writeQueueItem = ref sendQueueItem.GetUnsafeList()->ElementAt(sendQueueItem.Length - 1); + writeQueueItem.Writer.TryBeginWrite(tmpSerializer.Length + headerSerializer.Length); - writeQueueItem.Writer.WriteValue(header); - writeQueueItem.Writer.WriteBytes(tmpSerializer.GetUnsafePtr(), tmpSerializer.Length); - writeQueueItem.BatchHeader.BatchSize++; - for (var hookIdx = 0; hookIdx < m_Hooks.Count; ++hookIdx) - { - m_Hooks[hookIdx].OnAfterSendMessage(clientId, typeof(TMessageType), delivery, tmpSerializer.Length + FastBufferWriter.GetWriteSize()); - } + writeQueueItem.Writer.WriteBytes(headerSerializer.GetUnsafePtr(), headerSerializer.Length); + writeQueueItem.Writer.WriteBytes(tmpSerializer.GetUnsafePtr(), tmpSerializer.Length); + writeQueueItem.BatchHeader.BatchSize++; + for (var hookIdx = 0; hookIdx < m_Hooks.Count; ++hookIdx) + { + m_Hooks[hookIdx].OnAfterSendMessage(clientId, typeof(TMessageType), delivery, tmpSerializer.Length + headerSerializer.Length); } - - return tmpSerializer.Length + FastBufferWriter.GetWriteSize(); } + + return tmpSerializer.Length + headerSerializer.Length; } private struct PointerListWrapper : IReadOnlyList @@ -461,16 +491,16 @@ internal unsafe void ProcessSendQueues() try { m_MessageSender.Send(clientId, queueItem.NetworkDelivery, queueItem.Writer); + + for (var hookIdx = 0; hookIdx < m_Hooks.Count; ++hookIdx) + { + m_Hooks[hookIdx].OnAfterSendBatch(clientId, queueItem.BatchHeader.BatchSize, queueItem.Writer.Length, queueItem.NetworkDelivery); + } } finally { queueItem.Writer.Dispose(); } - - for (var hookIdx = 0; hookIdx < m_Hooks.Count; ++hookIdx) - { - m_Hooks[hookIdx].OnAfterSendBatch(clientId, queueItem.BatchHeader.BatchSize, queueItem.Writer.Length, queueItem.NetworkDelivery); - } } sendQueueItem.Clear(); } diff --git a/Runtime/Messaging/NetworkContext.cs b/Runtime/Messaging/NetworkContext.cs index 8adaffa..f588b5c 100644 --- a/Runtime/Messaging/NetworkContext.cs +++ b/Runtime/Messaging/NetworkContext.cs @@ -25,5 +25,10 @@ internal ref struct NetworkContext /// The header data that was sent with the message /// public MessageHeader Header; + + /// + /// The actual serialized size of the header when packed into the buffer + /// + public int SerializedHeaderSize; } } diff --git a/Runtime/Metrics/StreamExtensions.cs b/Runtime/Metrics/StreamExtensions.cs deleted file mode 100644 index 615efb5..0000000 --- a/Runtime/Metrics/StreamExtensions.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System.IO; - -namespace Unity.Netcode -{ - public static class StreamExtensions - { - public static long SafeGetLengthOrDefault(this Stream stream) - { - return stream.CanSeek ? stream.Length : 0; - } - } -} diff --git a/Runtime/NetworkVariable/NetworkVariable.cs b/Runtime/NetworkVariable/NetworkVariable.cs index 013877c..f247bac 100644 --- a/Runtime/NetworkVariable/NetworkVariable.cs +++ b/Runtime/NetworkVariable/NetworkVariable.cs @@ -9,6 +9,55 @@ namespace Unity.Netcode [Serializable] public class NetworkVariable : NetworkVariableBase where T : unmanaged { + // Functions that know how to serialize INetworkSerializable + internal static void WriteNetworkSerializable(FastBufferWriter writer, ref TForMethod value) + where TForMethod : INetworkSerializable, new() + { + writer.WriteNetworkSerializable(value); + } + internal static void ReadNetworkSerializable(FastBufferReader reader, out TForMethod value) + where TForMethod : INetworkSerializable, new() + { + reader.ReadNetworkSerializable(out value); + } + + // Functions that serialize other types + private static void WriteValue(FastBufferWriter writer, ref TForMethod value) where TForMethod : unmanaged + { + writer.WriteValueSafe(value); + } + + private static void ReadValue(FastBufferReader reader, out TForMethod value) + where TForMethod : unmanaged + { + reader.ReadValueSafe(out value); + } + + internal delegate void WriteDelegate(FastBufferWriter writer, ref TForMethod value); + + internal delegate void ReadDelegate(FastBufferReader reader, out TForMethod value); + + // These static delegates provide the right implementation for writing and reading a particular network variable + // type. + // + // For most types, these default to WriteValue() and ReadValue(), which perform simple memcpy operations. + // + // INetworkSerializableILPP will generate startup code that will set it to WriteNetworkSerializable() + // and ReadNetworkSerializable() for INetworkSerializable types, which will call NetworkSerialize(). + // + // In the future we may be able to use this to provide packing implementations for floats and integers to + // optimize bandwidth usage. + // + // The reason this is done is to avoid runtime reflection and boxing in NetworkVariable - without this, + // NetworkVariable would need to do a `var is INetworkSerializable` check, and then cast to INetworkSerializable, + // *both* of which would cause a boxing allocation. Alternatively, NetworkVariable could have been split into + // NetworkVariable and NetworkSerializableVariable or something like that, which would have caused a poor + // user experience and an API that's easier to get wrong than right. This is a bit ugly on the implementation + // side, but it gets the best achievable user experience and performance. + internal static WriteDelegate Write = WriteValue; + internal static ReadDelegate Read = ReadValue; + + /// /// Delegate type for value changed event /// @@ -106,7 +155,7 @@ public override void WriteDelta(FastBufferWriter writer) public override void ReadDelta(FastBufferReader reader, bool keepDirtyDelta) { T previousValue = m_InternalValue; - reader.ReadValueSafe(out m_InternalValue); + Read(reader, out m_InternalValue); if (keepDirtyDelta) { @@ -119,13 +168,13 @@ public override void ReadDelta(FastBufferReader reader, bool keepDirtyDelta) /// public override void ReadField(FastBufferReader reader) { - reader.ReadValueSafe(out m_InternalValue); + Read(reader, out m_InternalValue); } /// public override void WriteField(FastBufferWriter writer) { - writer.WriteValueSafe(m_InternalValue); + Write(writer, ref m_InternalValue); } } } diff --git a/Runtime/NetworkVariable/NetworkVariableHelper.cs b/Runtime/NetworkVariable/NetworkVariableHelper.cs new file mode 100644 index 0000000..1800910 --- /dev/null +++ b/Runtime/NetworkVariable/NetworkVariableHelper.cs @@ -0,0 +1,22 @@ +namespace Unity.Netcode +{ + public class NetworkVariableHelper + { + // This is called by ILPP during module initialization for all unmanaged INetworkSerializable types + // This sets up NetworkVariable so that it properly calls NetworkSerialize() when wrapping an INetworkSerializable value + // + // The reason this is done is to avoid runtime reflection and boxing in NetworkVariable - without this, + // NetworkVariable would need to do a `var is INetworkSerializable` check, and then cast to INetworkSerializable, + // *both* of which would cause a boxing allocation. Alternatively, NetworkVariable could have been split into + // NetworkVariable and NetworkSerializableVariable or something like that, which would have caused a poor + // user experience and an API that's easier to get wrong than right. This is a bit ugly on the implementation + // side, but it gets the best achievable user experience and performance. + // + // RuntimeAccessModifiersILPP will make this `public` + internal static void InitializeDelegates() where T : unmanaged, INetworkSerializable + { + NetworkVariable.Write = NetworkVariable.WriteNetworkSerializable; + NetworkVariable.Read = NetworkVariable.ReadNetworkSerializable; + } + } +} diff --git a/Runtime/Reflection/TypeExtensions.cs.meta b/Runtime/NetworkVariable/NetworkVariableHelper.cs.meta similarity index 83% rename from Runtime/Reflection/TypeExtensions.cs.meta rename to Runtime/NetworkVariable/NetworkVariableHelper.cs.meta index 80c06af..bbced6f 100644 --- a/Runtime/Reflection/TypeExtensions.cs.meta +++ b/Runtime/NetworkVariable/NetworkVariableHelper.cs.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: 3e168a2bc1a1e2642af0369780fb560c +guid: e54b65208bd3bbe4eaf62ca0384ae21f MonoImporter: externalObjects: {} serializedVersion: 2 diff --git a/Runtime/Reflection.meta b/Runtime/Reflection.meta deleted file mode 100644 index da604a3..0000000 --- a/Runtime/Reflection.meta +++ /dev/null @@ -1,8 +0,0 @@ -fileFormatVersion: 2 -guid: af81f9951b096ff4cb8e4f8a4106104a -folderAsset: yes -DefaultImporter: - externalObjects: {} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Runtime/Reflection/TypeExtensions.cs b/Runtime/Reflection/TypeExtensions.cs deleted file mode 100644 index 216e489..0000000 --- a/Runtime/Reflection/TypeExtensions.cs +++ /dev/null @@ -1,31 +0,0 @@ -using System; - -namespace Unity.Netcode -{ - internal static class TypeExtensions - { - internal static bool HasInterface(this Type type, Type interfaceType) - { - var ifaces = type.GetInterfaces(); - for (int i = 0; i < ifaces.Length; i++) - { - if (ifaces[i] == interfaceType) - { - return true; - } - } - - return false; - } - - internal static bool IsNullable(this Type type) - { - if (!type.IsValueType) - { - return true; // ref-type - } - - return Nullable.GetUnderlyingType(type) != null; - } - } -} diff --git a/Runtime/SceneManagement/NetworkSceneManager.cs b/Runtime/SceneManagement/NetworkSceneManager.cs index 889b1f8..ec1ef0a 100644 --- a/Runtime/SceneManagement/NetworkSceneManager.cs +++ b/Runtime/SceneManagement/NetworkSceneManager.cs @@ -1028,8 +1028,8 @@ private void OnSceneUnloaded(uint sceneEventId) // despawned that no longer exists SendSceneEventData(sceneEventId, m_NetworkManager.ConnectedClientsIds.Where(c => c != m_NetworkManager.ServerClientId).ToArray()); - //Second, server sets itself as having finished unloading - if (SceneEventProgressTracking.ContainsKey(sceneEventData.SceneEventProgressId)) + //Only if we are a host do we want register having loaded for the associated SceneEventProgress + if (SceneEventProgressTracking.ContainsKey(sceneEventData.SceneEventProgressId) && m_NetworkManager.IsHost) { SceneEventProgressTracking[sceneEventData.SceneEventProgressId].AddClientAsDone(m_NetworkManager.ServerClientId); } @@ -1344,8 +1344,8 @@ private void OnServerLoadedScene(uint sceneEventId, Scene scene) OnLoadComplete?.Invoke(m_NetworkManager.ServerClientId, SceneNameFromHash(sceneEventData.SceneHash), sceneEventData.LoadSceneMode); - //Second, set the server as having loaded for the associated SceneEventProgress - if (SceneEventProgressTracking.ContainsKey(sceneEventData.SceneEventProgressId)) + //Second, only if we are a host do we want register having loaded for the associated SceneEventProgress + if (SceneEventProgressTracking.ContainsKey(sceneEventData.SceneEventProgressId) && m_NetworkManager.IsHost) { SceneEventProgressTracking[sceneEventData.SceneEventProgressId].AddClientAsDone(m_NetworkManager.ServerClientId); } @@ -1830,21 +1830,18 @@ internal void HandleSceneEvent(ulong clientId, FastBufferReader reader) /// Moves all NetworkObjects that don't have the set to /// the "Do not destroy on load" scene. /// - private void MoveObjectsToDontDestroyOnLoad() + internal void MoveObjectsToDontDestroyOnLoad() { - // Move ALL NetworkObjects to the temp scene + // Move ALL NetworkObjects marked to persist scene transitions into the DDOL scene var objectsToKeep = new HashSet(m_NetworkManager.SpawnManager.SpawnedObjectsList); - foreach (var sobj in objectsToKeep) { - if (!sobj.DestroyWithScene || (sobj.IsSceneObject != null && sobj.IsSceneObject.Value && sobj.gameObject.scene == DontDestroyOnLoadScene)) + if (!sobj.DestroyWithScene || sobj.gameObject.scene == DontDestroyOnLoadScene) { - // Only move objects with no parent as child objects will follow - if (sobj.gameObject.transform.parent == null) + // Only move dynamically spawned network objects with no parent as child objects will follow + if (sobj.gameObject.transform.parent == null && sobj.IsSceneObject != null && !sobj.IsSceneObject.Value) { UnityEngine.Object.DontDestroyOnLoad(sobj.gameObject); - // Since we are doing a scene transition, disable the GameObject until the next scene is loaded - sobj.gameObject.SetActive(false); } } else if (m_NetworkManager.IsServer) @@ -1907,24 +1904,22 @@ private void PopulateScenePlacedObjects(Scene sceneToFilterBy, bool clearScenePl /// Moves all spawned NetworkObjects (from do not destroy on load) to the scene specified /// /// scene to move the NetworkObjects to - private void MoveObjectsFromDontDestroyOnLoadToScene(Scene scene) + internal void MoveObjectsFromDontDestroyOnLoadToScene(Scene scene) { // Move ALL NetworkObjects to the temp scene var objectsToKeep = m_NetworkManager.SpawnManager.SpawnedObjectsList; foreach (var sobj in objectsToKeep) { - if (sobj.gameObject.scene == DontDestroyOnLoadScene && (sobj.IsSceneObject == null || sobj.IsSceneObject.Value)) - { - continue; - } - - // Only move objects with no parent as child objects will follow - if (sobj.gameObject.transform.parent == null) + // If it is in the DDOL then + if (sobj.gameObject.scene == DontDestroyOnLoadScene) { - // set it back to active at this point - sobj.gameObject.SetActive(true); - SceneManager.MoveGameObjectToScene(sobj.gameObject, scene); + // only move dynamically spawned network objects, with no parent as child objects will follow, + // back into the currently active scene + if (sobj.gameObject.transform.parent == null && sobj.IsSceneObject != null && !sobj.IsSceneObject.Value) + { + SceneManager.MoveGameObjectToScene(sobj.gameObject, scene); + } } } } diff --git a/Runtime/Serialization/FastBufferWriter.cs b/Runtime/Serialization/FastBufferWriter.cs index 0d906b0..5875e92 100644 --- a/Runtime/Serialization/FastBufferWriter.cs +++ b/Runtime/Serialization/FastBufferWriter.cs @@ -189,7 +189,7 @@ internal unsafe void Grow(int additionalSizeRequired) var newSize = Math.Min(desiredSize, Handle->MaxCapacity); byte* newBuffer = (byte*)UnsafeUtility.Malloc(newSize, UnsafeUtility.AlignOf(), Handle->Allocator); #if DEVELOPMENT_BUILD || UNITY_EDITOR - UnsafeUtility.MemSet(newBuffer, 0, sizeof(WriterHandle) + newSize); + UnsafeUtility.MemSet(newBuffer, 0, newSize); #endif UnsafeUtility.MemCpy(newBuffer, Handle->BufferPointer, Length); if (Handle->BufferGrew) @@ -428,7 +428,7 @@ public void WriteNetworkSerializable(in T value) where T : INetworkSerializab /// /// /// - public void WriteNetworkSerializable(INetworkSerializable[] array, int count = -1, int offset = 0) where T : INetworkSerializable + public void WriteNetworkSerializable(T[] array, int count = -1, int offset = 0) where T : INetworkSerializable { int sizeInTs = count != -1 ? count : array.Length - offset; WriteValueSafe(sizeInTs); diff --git a/Runtime/Spawning/NetworkSpawnManager.cs b/Runtime/Spawning/NetworkSpawnManager.cs index ad10715..e111656 100644 --- a/Runtime/Spawning/NetworkSpawnManager.cs +++ b/Runtime/Spawning/NetworkSpawnManager.cs @@ -27,6 +27,7 @@ private struct TriggerData public MessageHeader Header; public ulong SenderId; public float Timestamp; + public int SerializedHeaderSize; } private struct TriggerInfo { @@ -117,7 +118,8 @@ internal unsafe void TriggerOnSpawn(ulong networkObjectId, FastBufferReader read Reader = new FastBufferReader(reader.GetUnsafePtr(), Allocator.Persistent, reader.Length), Header = context.Header, Timestamp = context.Timestamp, - SenderId = context.SenderId + SenderId = context.SenderId, + SerializedHeaderSize = context.SerializedHeaderSize }); } @@ -154,6 +156,24 @@ internal unsafe void CleanupStaleTriggers() m_Triggers.Remove(staleKeys[i]); } } + /// + /// Cleans up any trigger that's existed for more than a second. + /// These triggers were probably for situations where a request was received after a despawn rather than before a spawn. + /// + internal void CleanupAllTriggers() + { + foreach (var kvp in m_Triggers) + { + foreach (var data in kvp.Value.TriggerData) + { + data.Reader.Dispose(); + } + + kvp.Value.TriggerData.Dispose(); + } + + m_Triggers.Clear(); + } internal void RemoveOwnership(NetworkObject networkObject) { @@ -167,28 +187,44 @@ internal void RemoveOwnership(NetworkObject networkObject) throw new SpawnStateException("Object is not spawned"); } - for (int i = NetworkManager.ConnectedClients[networkObject.OwnerClientId].OwnedObjects.Count - 1; - i > -1; - i--) + // If we made it here then we are the server and if the server is determined to already be the owner + // then ignore the RemoveOwnership invocation. + if (networkObject.OwnerClientId == NetworkManager.ServerClientId) + { + return; + } + + // Make sure the connected client entry exists before trying to remove ownership. + if (TryGetNetworkClient(networkObject.OwnerClientId, out NetworkClient networkClient)) { - if (NetworkManager.ConnectedClients[networkObject.OwnerClientId].OwnedObjects[i] == networkObject) + for (int i = networkClient.OwnedObjects.Count - 1; i > -1; i--) { - NetworkManager.ConnectedClients[networkObject.OwnerClientId].OwnedObjects.RemoveAt(i); + if (networkClient.OwnedObjects[i] == networkObject) + { + networkClient.OwnedObjects.RemoveAt(i); + } } - } - networkObject.OwnerClientIdInternal = null; + networkObject.OwnerClientIdInternal = null; - var message = new ChangeOwnershipMessage - { - NetworkObjectId = networkObject.NetworkObjectId, - OwnerClientId = networkObject.OwnerClientId - }; - var size = NetworkManager.SendMessage(message, NetworkDelivery.ReliableSequenced, NetworkManager.ConnectedClientsIds); + var message = new ChangeOwnershipMessage + { + NetworkObjectId = networkObject.NetworkObjectId, + OwnerClientId = networkObject.OwnerClientId + }; + var size = NetworkManager.SendMessage(message, NetworkDelivery.ReliableSequenced, NetworkManager.ConnectedClientsIds); - foreach (var client in NetworkManager.ConnectedClients) + foreach (var client in NetworkManager.ConnectedClients) + { + NetworkManager.NetworkMetrics.TrackOwnershipChangeSent(client.Key, networkObject, size); + } + } + else { - NetworkManager.NetworkMetrics.TrackOwnershipChangeSent(client.Key, networkObject, size); + if (NetworkLog.CurrentLogLevel <= LogLevel.Normal) + { + NetworkLog.LogWarning($"No connected clients prior to removing ownership for {networkObject.name}. Make sure you are not initializing or shutting down when removing ownership."); + } } } @@ -207,7 +243,7 @@ private bool TryGetNetworkClient(ulong clientId, out NetworkClient networkClient return NetworkManager.ConnectedClients.TryGetValue(clientId, out networkClient); } - if (clientId == NetworkManager.LocalClient.ClientId) + if (NetworkManager.LocalClient != null && clientId == NetworkManager.LocalClient.ClientId) { networkClient = NetworkManager.LocalClient; return true; @@ -483,7 +519,7 @@ private void SpawnNetworkObjectLocallyCommon(NetworkObject networkObject, ulong foreach (var trigger in triggerInfo.TriggerData) { // Reader will be disposed within HandleMessage - NetworkManager.MessagingSystem.HandleMessage(trigger.Header, trigger.Reader, trigger.SenderId, trigger.Timestamp); + NetworkManager.MessagingSystem.HandleMessage(trigger.Header, trigger.Reader, trigger.SenderId, trigger.Timestamp, trigger.SerializedHeaderSize); } triggerInfo.TriggerData.Dispose(); @@ -580,7 +616,7 @@ internal void ServerDestroySpawnedSceneObjects() } } - internal void DestroyNonSceneObjects() + internal void DespawnAndDestroyNetworkObjects() { var networkObjects = UnityEngine.Object.FindObjectsOfType(); @@ -588,17 +624,25 @@ internal void DestroyNonSceneObjects() { if (networkObjects[i].NetworkManager == NetworkManager) { - if (networkObjects[i].IsSceneObject != null && networkObjects[i].IsSceneObject.Value == false) + if (NetworkManager.PrefabHandler.ContainsHandler(networkObjects[i])) { - if (NetworkManager.PrefabHandler.ContainsHandler(networkObjects[i])) - { - NetworkManager.PrefabHandler.HandleNetworkPrefabDestroy(networkObjects[i]); - OnDespawnObject(networkObjects[i], false); - } - else - { - UnityEngine.Object.Destroy(networkObjects[i].gameObject); - } + OnDespawnObject(networkObjects[i], false); + // Leave destruction up to the handler + NetworkManager.PrefabHandler.HandleNetworkPrefabDestroy(networkObjects[i]); + } + else if (networkObjects[i].IsSpawned) + { + // If it is an in-scene placed NetworkObject then just despawn + // and let it be destroyed when the scene is unloaded. Otherwise, + // despawn and destroy it. + var shouldDestroy = !(networkObjects[i].IsSceneObject != null + && networkObjects[i].IsSceneObject.Value); + + OnDespawnObject(networkObjects[i], shouldDestroy); + } + else + { + UnityEngine.Object.Destroy(networkObjects[i].gameObject); } } } @@ -668,17 +712,21 @@ internal void OnDespawnObject(NetworkObject networkObject, bool destroyGameObjec return; } - // Move child NetworkObjects to the root when parent NetworkObject is destroyed - foreach (var spawnedNetObj in SpawnedObjectsList) + // If we are shutting down the NetworkManager, then ignore resetting the parent + if (!NetworkManager.ShutdownInProgress) { - var (isReparented, latestParent) = spawnedNetObj.GetNetworkParenting(); - if (isReparented && latestParent == networkObject.NetworkObjectId) + // Move child NetworkObjects to the root when parent NetworkObject is destroyed + foreach (var spawnedNetObj in SpawnedObjectsList) { - spawnedNetObj.gameObject.transform.parent = null; - - if (NetworkLog.CurrentLogLevel <= LogLevel.Normal) + var (isReparented, latestParent) = spawnedNetObj.GetNetworkParenting(); + if (isReparented && latestParent == networkObject.NetworkObjectId) { - NetworkLog.LogWarning($"{nameof(NetworkObject)} #{spawnedNetObj.NetworkObjectId} moved to the root because its parent {nameof(NetworkObject)} #{networkObject.NetworkObjectId} is destroyed"); + spawnedNetObj.gameObject.transform.parent = null; + + if (NetworkLog.CurrentLogLevel <= LogLevel.Normal) + { + NetworkLog.LogWarning($"{nameof(NetworkObject)} #{spawnedNetObj.NetworkObjectId} moved to the root because its parent {nameof(NetworkObject)} #{networkObject.NetworkObjectId} is destroyed"); + } } } } diff --git a/Tests/Editor/Messaging/MessageReceivingTests.cs b/Tests/Editor/Messaging/MessageReceivingTests.cs index ef8bfda..90dc911 100644 --- a/Tests/Editor/Messaging/MessageReceivingTests.cs +++ b/Tests/Editor/Messaging/MessageReceivingTests.cs @@ -92,7 +92,7 @@ public void WhenHandlingAMessage_ReceiveMethodIsCalled() var reader = new FastBufferReader(writer, Allocator.Temp); using (reader) { - m_MessagingSystem.HandleMessage(messageHeader, reader, 0, 0); + m_MessagingSystem.HandleMessage(messageHeader, reader, 0, 0, 0); Assert.IsTrue(TestMessage.Deserialized); Assert.AreEqual(1, TestMessage.DeserializedValues.Count); Assert.AreEqual(message, TestMessage.DeserializedValues[0]); @@ -143,7 +143,7 @@ public void WhenReceivingAMessageAndProcessingMessageQueue_ReceiveMethodIsCalled }; var messageHeader = new MessageHeader { - MessageSize = (ushort)UnsafeUtility.SizeOf(), + MessageSize = (uint)UnsafeUtility.SizeOf(), MessageType = m_MessagingSystem.GetMessageType(typeof(TestMessage)), }; var message = GetMessage(); @@ -151,12 +151,10 @@ public void WhenReceivingAMessageAndProcessingMessageQueue_ReceiveMethodIsCalled var writer = new FastBufferWriter(1300, Allocator.Temp); using (writer) { - writer.TryBeginWrite(FastBufferWriter.GetWriteSize(batchHeader) + - FastBufferWriter.GetWriteSize(messageHeader) + - FastBufferWriter.GetWriteSize(message)); - writer.WriteValue(batchHeader); - writer.WriteValue(messageHeader); - writer.WriteValue(message); + writer.WriteValueSafe(batchHeader); + BytePacker.WriteValueBitPacked(writer, messageHeader.MessageType); + BytePacker.WriteValueBitPacked(writer, messageHeader.MessageSize); + writer.WriteValueSafe(message); var reader = new FastBufferReader(writer, Allocator.Temp); using (reader) @@ -188,14 +186,13 @@ public void WhenReceivingMultipleMessagesAndProcessingMessageQueue_ReceiveMethod var writer = new FastBufferWriter(1300, Allocator.Temp); using (writer) { - writer.TryBeginWrite(FastBufferWriter.GetWriteSize(batchHeader) + - FastBufferWriter.GetWriteSize(messageHeader) * 2 + - FastBufferWriter.GetWriteSize(message) * 2); - writer.WriteValue(batchHeader); - writer.WriteValue(messageHeader); - writer.WriteValue(message); - writer.WriteValue(messageHeader); - writer.WriteValue(message2); + writer.WriteValueSafe(batchHeader); + BytePacker.WriteValueBitPacked(writer, messageHeader.MessageType); + BytePacker.WriteValueBitPacked(writer, messageHeader.MessageSize); + writer.WriteValueSafe(message); + BytePacker.WriteValueBitPacked(writer, messageHeader.MessageType); + BytePacker.WriteValueBitPacked(writer, messageHeader.MessageSize); + writer.WriteValueSafe(message2); var reader = new FastBufferReader(writer, Allocator.Temp); using (reader) diff --git a/Tests/Editor/Messaging/MessageSendingTests.cs b/Tests/Editor/Messaging/MessageSendingTests.cs index d23c18a..ebf6b21 100644 --- a/Tests/Editor/Messaging/MessageSendingTests.cs +++ b/Tests/Editor/Messaging/MessageSendingTests.cs @@ -124,7 +124,7 @@ public void WhenSendingMultipleMessages_MessagesAreBatched() public void WhenNotExceedingBatchSize_NewBatchesAreNotCreated() { var message = GetMessage(); - var size = UnsafeUtility.SizeOf() + UnsafeUtility.SizeOf(); + var size = UnsafeUtility.SizeOf() + 2; // MessageHeader packed with this message will be 2 bytes for (var i = 0; i < 1300 / size; ++i) { m_MessagingSystem.SendMessage(message, NetworkDelivery.Reliable, m_Clients); @@ -138,7 +138,7 @@ public void WhenNotExceedingBatchSize_NewBatchesAreNotCreated() public void WhenExceedingBatchSize_NewBatchesAreCreated() { var message = GetMessage(); - var size = UnsafeUtility.SizeOf() + UnsafeUtility.SizeOf(); + var size = UnsafeUtility.SizeOf() + 2; // MessageHeader packed with this message will be 2 bytes for (var i = 0; i < (1300 / size) + 1; ++i) { m_MessagingSystem.SendMessage(message, NetworkDelivery.Reliable, m_Clients); @@ -152,7 +152,7 @@ public void WhenExceedingBatchSize_NewBatchesAreCreated() public void WhenExceedingMTUSizeWithFragmentedDelivery_NewBatchesAreNotCreated() { var message = GetMessage(); - var size = UnsafeUtility.SizeOf() + UnsafeUtility.SizeOf(); + var size = UnsafeUtility.SizeOf() + 2; // MessageHeader packed with this message will be 2 bytes for (var i = 0; i < (1300 / size) + 1; ++i) { m_MessagingSystem.SendMessage(message, NetworkDelivery.ReliableFragmentedSequenced, m_Clients); @@ -198,24 +198,25 @@ public void WhenSendingMessaged_SentDataIsCorrect() var reader = new FastBufferReader(m_MessageSender.MessageQueue[0], Allocator.Temp); using (reader) { - reader.TryBeginRead( - FastBufferWriter.GetWriteSize() + - FastBufferWriter.GetWriteSize() * 2 + - FastBufferWriter.GetWriteSize() * 2 - ); - reader.ReadValue(out BatchHeader header); + reader.ReadValueSafe(out BatchHeader header); Assert.AreEqual(2, header.BatchSize); - reader.ReadValue(out MessageHeader messageHeader); + MessageHeader messageHeader; + + ByteUnpacker.ReadValueBitPacked(reader, out messageHeader.MessageType); + ByteUnpacker.ReadValueBitPacked(reader, out messageHeader.MessageSize); + Assert.AreEqual(m_MessagingSystem.GetMessageType(typeof(TestMessage)), messageHeader.MessageType); Assert.AreEqual(UnsafeUtility.SizeOf(), messageHeader.MessageSize); - reader.ReadValue(out TestMessage receivedMessage); + reader.ReadValueSafe(out TestMessage receivedMessage); Assert.AreEqual(message, receivedMessage); - reader.ReadValue(out MessageHeader messageHeader2); - Assert.AreEqual(m_MessagingSystem.GetMessageType(typeof(TestMessage)), messageHeader2.MessageType); - Assert.AreEqual(UnsafeUtility.SizeOf(), messageHeader2.MessageSize); - reader.ReadValue(out TestMessage receivedMessage2); + ByteUnpacker.ReadValueBitPacked(reader, out messageHeader.MessageType); + ByteUnpacker.ReadValueBitPacked(reader, out messageHeader.MessageSize); + + Assert.AreEqual(m_MessagingSystem.GetMessageType(typeof(TestMessage)), messageHeader.MessageType); + Assert.AreEqual(UnsafeUtility.SizeOf(), messageHeader.MessageSize); + reader.ReadValueSafe(out TestMessage receivedMessage2); Assert.AreEqual(message2, receivedMessage2); } } diff --git a/Tests/Editor/SnapshotRttTests.cs b/Tests/Editor/SnapshotRttTests.cs index bb3aa7b..697af30 100644 --- a/Tests/Editor/SnapshotRttTests.cs +++ b/Tests/Editor/SnapshotRttTests.cs @@ -9,7 +9,7 @@ public class SnapshotRttTests [Test] public void TestBasicRtt() { - var snapshot = new SnapshotSystem(default); + var snapshot = new SnapshotSystem(null, new NetworkConfig(), null); var client1 = snapshot.GetConnectionRtt(0); client1.NotifySend(0, 0.0); @@ -40,7 +40,7 @@ public void TestBasicRtt() [Test] public void TestEdgeCasesRtt() { - var snapshot = new SnapshotSystem(NetworkManager.Singleton); + var snapshot = new SnapshotSystem(null, new NetworkConfig(), null); var client1 = snapshot.GetConnectionRtt(0); var iterationCount = NetworkConfig.RttWindowSize * 3; var extraCount = NetworkConfig.RttWindowSize * 2; diff --git a/Tests/Editor/SnapshotTests.cs b/Tests/Editor/SnapshotTests.cs new file mode 100644 index 0000000..2ab3354 --- /dev/null +++ b/Tests/Editor/SnapshotTests.cs @@ -0,0 +1,372 @@ +using System; +using System.Collections.Generic; +using Unity.Collections; +using UnityEngine; +using NUnit.Framework; +using Random = System.Random; + +namespace Unity.Netcode.EditorTests +{ + public class SnapshotTests + { + private SnapshotSystem m_SendSnapshot; + private SnapshotSystem m_RecvSnapshot; + + private NetworkTimeSystem m_SendTimeSystem; + private NetworkTickSystem m_SendTickSystem; + private NetworkTimeSystem m_RecvTimeSystem; + private NetworkTickSystem m_RecvTickSystem; + + private int m_SpawnedObjectCount; + private int m_DespawnedObjectCount; + private int m_NextSequence; + private uint m_TicksPerSec = 15; + private int m_MinSpawns; + private int m_MinDespawns; + + private bool m_ExpectSpawns; + private bool m_ExpectDespawns; + private bool m_LoseNextMessage; + private bool m_PassBackResponses; + + public void Prepare() + { + PrepareSendSideSnapshot(); + PrepareRecvSideSnapshot(); + } + + public void AdvanceOneTickSendSide() + { + m_SendTimeSystem.Advance(1.0f / m_TicksPerSec); + m_SendTickSystem.UpdateTick(m_SendTimeSystem.LocalTime, m_SendTimeSystem.ServerTime); + m_SendSnapshot.NetworkUpdate(NetworkUpdateStage.EarlyUpdate); + } + + public void AdvanceOneTickRecvSide() + { + m_RecvTimeSystem.Advance(1.0f / m_TicksPerSec); + m_RecvTickSystem.UpdateTick(m_RecvTimeSystem.LocalTime, m_RecvTimeSystem.ServerTime); + m_RecvSnapshot.NetworkUpdate(NetworkUpdateStage.EarlyUpdate); + } + + public void AdvanceOneTick() + { + AdvanceOneTickSendSide(); + AdvanceOneTickRecvSide(); + } + + internal int SpawnObject(SnapshotSpawnCommand command) + { + m_SpawnedObjectCount++; + return 0; + } + + internal int DespawnObject(SnapshotDespawnCommand command) + { + m_DespawnedObjectCount++; + return 0; + } + + internal int SendMessage(in SnapshotDataMessage message, NetworkDelivery delivery, ulong clientId) + { + if (!m_PassBackResponses) + { + // we're not ack'ing anything, so those should stay 0 + Debug.Assert(message.Ack.LastReceivedSequence == 0); + } + + Debug.Assert(message.Ack.ReceivedSequenceMask == 0); + Debug.Assert(message.Sequence == m_NextSequence); // sequence has to be the expected one + + if (m_ExpectSpawns) + { + Debug.Assert(message.Spawns.Length >= m_MinSpawns); // there has to be multiple spawns per SnapshotMessage + } + else + { + Debug.Assert(message.Spawns.Length == 0); // Spawns were not expected + } + + if (m_ExpectDespawns) + { + Debug.Assert(message.Despawns.Length >= m_MinDespawns); // there has to be multiple despawns per SnapshotMessage + } + else + { + Debug.Assert(message.Despawns.IsEmpty); // this test should not have despawns + } + + Debug.Assert(message.Entries.Length == 0); + + m_NextSequence++; + + if (!m_LoseNextMessage) + { + using var writer = new FastBufferWriter(1024, Allocator.Temp); + message.Serialize(writer); + using var reader = new FastBufferReader(writer, Allocator.Temp); + var context = new NetworkContext { SenderId = 0, Timestamp = 0.0f, SystemOwner = new Tuple(m_RecvSnapshot, 0) }; + SnapshotDataMessage.Receive(reader, context); + } + else + { + message.Spawns.Dispose(); + message.Despawns.Dispose(); + message.Entries.Dispose(); + } + + return 0; + } + + internal int SendMessageRecvSide(in SnapshotDataMessage message, NetworkDelivery delivery, ulong clientId) + { + if (m_PassBackResponses) + { + using var writer = new FastBufferWriter(1024, Allocator.Temp); + message.Serialize(writer); + using var reader = new FastBufferReader(writer, Allocator.Temp); + var context = new NetworkContext { SenderId = 0, Timestamp = 0.0f, SystemOwner = new Tuple(m_SendSnapshot, 1) }; + SnapshotDataMessage.Receive(reader, context); + } + else + { + message.Spawns.Dispose(); + message.Despawns.Dispose(); + message.Entries.Dispose(); + } + + return 0; + } + + + private void PrepareSendSideSnapshot() + { + var config = new NetworkConfig(); + + m_SendTickSystem = new NetworkTickSystem(m_TicksPerSec, 0.0, 0.0); + m_SendTimeSystem = new NetworkTimeSystem(0.2, 0.2, 1.0); + + config.UseSnapshotDelta = false; + config.UseSnapshotSpawn = true; + + m_SendSnapshot = new SnapshotSystem(null, config, m_SendTickSystem); + + m_SendSnapshot.IsServer = true; + m_SendSnapshot.IsConnectedClient = false; + m_SendSnapshot.ServerClientId = 0; + m_SendSnapshot.ConnectedClientsId.Clear(); + m_SendSnapshot.ConnectedClientsId.Add(0); + m_SendSnapshot.ConnectedClientsId.Add(1); + m_SendSnapshot.MockSendMessage = SendMessage; + m_SendSnapshot.MockSpawnObject = SpawnObject; + m_SendSnapshot.MockDespawnObject = DespawnObject; + } + + private void PrepareRecvSideSnapshot() + { + var config = new NetworkConfig(); + + m_RecvTickSystem = new NetworkTickSystem(m_TicksPerSec, 0.0, 0.0); + m_RecvTimeSystem = new NetworkTimeSystem(0.2, 0.2, 1.0); + + config.UseSnapshotDelta = false; + config.UseSnapshotSpawn = true; + + m_RecvSnapshot = new SnapshotSystem(null, config, m_RecvTickSystem); + + m_RecvSnapshot.IsServer = false; + m_RecvSnapshot.IsConnectedClient = true; + m_RecvSnapshot.ServerClientId = 0; + m_RecvSnapshot.ConnectedClientsId.Clear(); + m_SendSnapshot.ConnectedClientsId.Add(0); + m_SendSnapshot.ConnectedClientsId.Add(1); + m_RecvSnapshot.MockSendMessage = SendMessageRecvSide; + m_RecvSnapshot.MockSpawnObject = SpawnObject; + m_RecvSnapshot.MockDespawnObject = DespawnObject; + } + + private void SendSpawnToSnapshot(ulong objectId) + { + SnapshotSpawnCommand command = default; + // identity + command.NetworkObjectId = objectId; + // archetype + command.GlobalObjectIdHash = 0; + command.IsSceneObject = true; + // parameters + command.IsPlayerObject = false; + command.OwnerClientId = 0; + command.ParentNetworkId = 0; + command.ObjectPosition = default; + command.ObjectRotation = default; + command.ObjectScale = new Vector3(1.0f, 1.0f, 1.0f); + command.TargetClientIds = new List { 1 }; + m_SendSnapshot.Spawn(command); + } + + private void SendDespawnToSnapshot(ulong objectId) + { + SnapshotDespawnCommand command = default; + // identity + command.NetworkObjectId = objectId; + command.TargetClientIds = new List { 1 }; + m_SendSnapshot.Despawn(command); + } + + [Test] + public void TestSnapshotSpawn() + { + Prepare(); + + m_SpawnedObjectCount = 0; + m_NextSequence = 0; + m_ExpectSpawns = true; + m_ExpectDespawns = false; + m_MinSpawns = 2; // many spawns are to be sent together + m_LoseNextMessage = false; + m_PassBackResponses = false; + + var ticksToRun = 20; + + // spawns one more than current buffer size + var objectsToSpawn = m_SendSnapshot.SpawnsBufferCount + 1; + + for (int i = 0; i < objectsToSpawn; i++) + { + SendSpawnToSnapshot((ulong)i); + } + + for (int i = 0; i < ticksToRun; i++) + { + AdvanceOneTick(); + } + + Debug.Assert(m_SpawnedObjectCount == objectsToSpawn); + Debug.Assert(m_SendSnapshot.SpawnsBufferCount > objectsToSpawn); // spawn buffer should have grown + } + + [Test] + public void TestSnapshotSpawnDespawns() + { + Prepare(); + + // test that buffers actually shrink and will grow back to needed size + m_SendSnapshot.ReduceBufferUsage(); + m_RecvSnapshot.ReduceBufferUsage(); + Debug.Assert(m_SendSnapshot.SpawnsBufferCount == 1); + Debug.Assert(m_SendSnapshot.DespawnsBufferCount == 1); + Debug.Assert(m_RecvSnapshot.SpawnsBufferCount == 1); + Debug.Assert(m_RecvSnapshot.DespawnsBufferCount == 1); + + m_SpawnedObjectCount = 0; + m_DespawnedObjectCount = 0; + + m_NextSequence = 0; + m_ExpectSpawns = true; + m_ExpectDespawns = false; + m_MinDespawns = 2; // many despawns are to be sent together + m_LoseNextMessage = false; + m_PassBackResponses = false; + + var ticksToRun = 20; + + // spawns one more than current buffer size + var objectsToSpawn = 10; + + for (int i = 0; i < objectsToSpawn; i++) + { + SendSpawnToSnapshot((ulong)i); + } + + for (int i = 0; i < ticksToRun; i++) + { + AdvanceOneTick(); + } + + for (int i = 0; i < objectsToSpawn; i++) + { + SendDespawnToSnapshot((ulong)i); + } + + m_ExpectSpawns = true; // the un'acked spawns will still be present + m_MinSpawns = 1; // but we don't really care how they are grouped then + m_ExpectDespawns = true; + + for (int i = 0; i < ticksToRun; i++) + { + AdvanceOneTick(); + } + + Debug.Assert(m_DespawnedObjectCount == objectsToSpawn); + } + + [Test] + public void TestSnapshotMessageLoss() + { + var r = new Random(); + Prepare(); + + m_SpawnedObjectCount = 0; + m_NextSequence = 0; + m_ExpectSpawns = true; + m_ExpectDespawns = false; + m_MinSpawns = 1; + m_LoseNextMessage = false; + m_PassBackResponses = false; + + var ticksToRun = 10; + + for (int i = 0; i < ticksToRun; i++) + { + m_LoseNextMessage = (r.Next() % 2) > 0; + + SendSpawnToSnapshot((ulong)i); + AdvanceOneTick(); + } + + m_LoseNextMessage = false; + AdvanceOneTick(); + AdvanceOneTick(); + + Debug.Assert(m_SpawnedObjectCount == ticksToRun); + } + + [Test] + public void TestSnapshotAcks() + { + Prepare(); + + m_SpawnedObjectCount = 0; + m_NextSequence = 0; + m_ExpectSpawns = true; + m_ExpectDespawns = false; + m_MinSpawns = 1; + m_LoseNextMessage = false; + m_PassBackResponses = true; + + var objectsToSpawn = 10; + + for (int i = 0; i < objectsToSpawn; i++) + { + SendSpawnToSnapshot((ulong)i); + } + AdvanceOneTickSendSide(); // let's tick the send multiple time, to check it still tries to send + AdvanceOneTick(); + + m_ExpectSpawns = false; // all spawns should have made it back and forth and be absent from next messages + AdvanceOneTick(); + + for (int i = 0; i < objectsToSpawn; i++) + { + SendDespawnToSnapshot((ulong)i); + } + + m_ExpectDespawns = true; // we should now be seeing despawns + AdvanceOneTickSendSide(); // let's tick the send multiple time, to check it still tries to send + AdvanceOneTick(); + + Debug.Assert(m_SpawnedObjectCount == objectsToSpawn); + } + } +} + diff --git a/Runtime/Metrics/StreamExtensions.cs.meta b/Tests/Editor/SnapshotTests.cs.meta similarity index 83% rename from Runtime/Metrics/StreamExtensions.cs.meta rename to Tests/Editor/SnapshotTests.cs.meta index 3eb9911..294c508 100644 --- a/Runtime/Metrics/StreamExtensions.cs.meta +++ b/Tests/Editor/SnapshotTests.cs.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: 61dd9b1558f6d7c46ad323b2c2c03c29 +guid: 3d41788be1de34b7c8bcfce6a2877754 MonoImporter: externalObjects: {} serializedVersion: 2 diff --git a/Tests/Editor/StartStopTests.cs b/Tests/Editor/StartStopTests.cs index 9db67ae..f232795 100644 --- a/Tests/Editor/StartStopTests.cs +++ b/Tests/Editor/StartStopTests.cs @@ -45,13 +45,26 @@ public void TestStartupServerState() public void TestFlagShutdown() { m_NetworkManager.StartServer(); - m_NetworkManager.Shutdown(); + m_NetworkManager.ShutdownInternal(); Assert.False(m_NetworkManager.IsServer); Assert.False(m_NetworkManager.IsClient); Assert.False(m_NetworkManager.IsHost); } + [Test] + public void TestShutdownWithoutStartForExceptions() + { + m_NetworkManager.ShutdownInternal(); + } + + [Test] + public void TestShutdownWithoutConfigForExceptions() + { + m_NetworkManager.NetworkConfig = null; + m_NetworkManager.ShutdownInternal(); + } + [TearDown] public void Teardown() { diff --git a/Tests/Editor/com.unity.netcode.editortests.asmdef b/Tests/Editor/com.unity.netcode.editortests.asmdef index d70259e..25626d1 100644 --- a/Tests/Editor/com.unity.netcode.editortests.asmdef +++ b/Tests/Editor/com.unity.netcode.editortests.asmdef @@ -2,6 +2,7 @@ "name": "Unity.Netcode.EditorTests", "rootNamespace": "Unity.Netcode.EditorTests", "references": [ + "Unity.Collections", "Unity.Netcode.Runtime", "Unity.Netcode.Editor", "Unity.Netcode.Components", diff --git a/Tests/Runtime/Metrics/MessagingMetricsTests.cs b/Tests/Runtime/Metrics/MessagingMetricsTests.cs index 675e08c..ad3cec2 100644 --- a/Tests/Runtime/Metrics/MessagingMetricsTests.cs +++ b/Tests/Runtime/Metrics/MessagingMetricsTests.cs @@ -15,8 +15,10 @@ namespace Unity.Netcode.RuntimeTests.Metrics public class MessagingMetricsTests : DualClientMetricTestBase { private const uint k_MessageNameHashSize = 8; - private static readonly int k_NamedMessageOverhead = (int)k_MessageNameHashSize + FastBufferWriter.GetWriteSize(); - private static readonly int k_UnnamedMessageOverhead = FastBufferWriter.GetWriteSize(); + // Header is dynamically sized due to packing, will be 2 bytes for all test messages. + private const int k_MessageHeaderSize = 2; + private static readonly int k_NamedMessageOverhead = (int)k_MessageNameHashSize + k_MessageHeaderSize; + private static readonly int k_UnnamedMessageOverhead = k_MessageHeaderSize; protected override int NbClients => 2; diff --git a/Tests/Runtime/Metrics/OwnershipChangeMetricsTests.cs b/Tests/Runtime/Metrics/OwnershipChangeMetricsTests.cs index 31151d2..853bd73 100644 --- a/Tests/Runtime/Metrics/OwnershipChangeMetricsTests.cs +++ b/Tests/Runtime/Metrics/OwnershipChangeMetricsTests.cs @@ -14,6 +14,8 @@ internal class OwnershipChangeMetricsTests : SingleClientMetricTestBase { private const string k_NewNetworkObjectName = "TestNetworkObjectToSpawn"; private NetworkObject m_NewNetworkPrefab; + // Header is dynamically sized due to packing, will be 2 bytes for all test messages. + private const int k_MessageHeaderSize = 2; protected override Action UpdatePlayerPrefab => _ => { @@ -58,7 +60,7 @@ public IEnumerator TrackOwnershipChangeSentMetric() var ownershipChangeSent = metricValues.First(); Assert.AreEqual(networkObject.NetworkObjectId, ownershipChangeSent.NetworkId.NetworkId); Assert.AreEqual(Server.LocalClientId, ownershipChangeSent.Connection.Id); - Assert.AreEqual(FastBufferWriter.GetWriteSize() + FastBufferWriter.GetWriteSize(), ownershipChangeSent.BytesCount); + Assert.AreEqual(FastBufferWriter.GetWriteSize() + k_MessageHeaderSize, ownershipChangeSent.BytesCount); } [UnityTest] diff --git a/Tests/Runtime/Metrics/ServerLogsMetricTests.cs b/Tests/Runtime/Metrics/ServerLogsMetricTests.cs index 6471cbf..8036dfd 100644 --- a/Tests/Runtime/Metrics/ServerLogsMetricTests.cs +++ b/Tests/Runtime/Metrics/ServerLogsMetricTests.cs @@ -11,7 +11,9 @@ namespace Unity.Netcode.RuntimeTests.Metrics { internal class ServerLogsMetricTests : SingleClientMetricTestBase { - private static readonly int k_ServerLogSentMessageOverhead = 2 + FastBufferWriter.GetWriteSize(); + // Header is dynamically sized due to packing, will be 2 bytes for all test messages. + private const int k_MessageHeaderSize = 2; + private static readonly int k_ServerLogSentMessageOverhead = 2 + k_MessageHeaderSize; private static readonly int k_ServerLogReceivedMessageOverhead = 2; [UnityTest] diff --git a/Tests/Runtime/Metrics/TransportBytesMetricsTests.cs b/Tests/Runtime/Metrics/TransportBytesMetricsTests.cs index 6ba1029..2e8986c 100644 --- a/Tests/Runtime/Metrics/TransportBytesMetricsTests.cs +++ b/Tests/Runtime/Metrics/TransportBytesMetricsTests.cs @@ -13,7 +13,9 @@ namespace Unity.Netcode.RuntimeTests.Metrics { internal class TransportBytesMetricsTests : SingleClientMetricTestBase { - static readonly long MessageOverhead = 8 + FastBufferWriter.GetWriteSize() + FastBufferWriter.GetWriteSize(); + // Header is dynamically sized due to packing, will be 2 bytes for all test messages. + private const int k_MessageHeaderSize = 2; + static readonly long MessageOverhead = 8 + FastBufferWriter.GetWriteSize() + k_MessageHeaderSize; [UnityTest] public IEnumerator TrackTotalNumberOfBytesSent() diff --git a/Tests/Runtime/MultiInstanceHelpers.cs b/Tests/Runtime/MultiInstanceHelpers.cs index bfcc44d..70abe74 100644 --- a/Tests/Runtime/MultiInstanceHelpers.cs +++ b/Tests/Runtime/MultiInstanceHelpers.cs @@ -450,7 +450,7 @@ public static IEnumerator RunAndWaitForCondition(Action workload, Func pre if (!waitResult.Result) { - throw new Exception(); + Assert.Fail("Predicate condition failed"); } } diff --git a/Tests/Runtime/NetworkBehaviourUpdaterTests.cs b/Tests/Runtime/NetworkBehaviourUpdaterTests.cs index 0863787..7ebec09 100644 --- a/Tests/Runtime/NetworkBehaviourUpdaterTests.cs +++ b/Tests/Runtime/NetworkBehaviourUpdaterTests.cs @@ -103,6 +103,8 @@ void AddNetworkBehaviour(Type type, GameObject prefab) var serverNetVarCount = serverNetVarsToUpdate.Count; yield return new WaitForSeconds(0); // wait a frame to make sure spawn is done + // todo: with Snapshot spawns enabled and the current race condition, the following line is needed: + // yield return new WaitForSeconds(0.2f); // wait a bit to fix the spawn/update race condition foreach (var netVar in serverNetVarsToUpdate) { diff --git a/Tests/Runtime/NetworkObject/NetworkObjectOnNetworkDespawnTests.cs b/Tests/Runtime/NetworkObject/NetworkObjectOnNetworkDespawnTests.cs new file mode 100644 index 0000000..8a15e2e --- /dev/null +++ b/Tests/Runtime/NetworkObject/NetworkObjectOnNetworkDespawnTests.cs @@ -0,0 +1,132 @@ +using System.Collections; +using NUnit.Framework; +using UnityEngine; +using UnityEngine.TestTools; + + +namespace Unity.Netcode.RuntimeTests +{ + /// + /// Tests that check OnNetworkDespawn being invoked + /// + public class NetworkObjectOnNetworkDespawnTests + { + private NetworkManager m_ServerHost; + private NetworkManager[] m_Clients; + + private GameObject m_ObjectToSpawn; + private NetworkObject m_NetworkObject; + + internal class OnNetworkDespawnTestComponent : NetworkBehaviour + { + public bool OnNetworkDespawnCalled { get; internal set; } + + public override void OnNetworkSpawn() + { + OnNetworkDespawnCalled = false; + base.OnNetworkSpawn(); + } + + public override void OnNetworkDespawn() + { + OnNetworkDespawnCalled = true; + base.OnNetworkDespawn(); + } + } + + [UnitySetUp] + public IEnumerator Setup() + { + Assert.IsTrue(MultiInstanceHelpers.Create(1, out m_ServerHost, out m_Clients)); + + m_ObjectToSpawn = new GameObject(); + m_NetworkObject = m_ObjectToSpawn.AddComponent(); + m_ObjectToSpawn.AddComponent(); + + // Make it a prefab + MultiInstanceHelpers.MakeNetworkObjectTestPrefab(m_NetworkObject); + + var networkPrefab = new NetworkPrefab(); + networkPrefab.Prefab = m_ObjectToSpawn; + m_ServerHost.NetworkConfig.NetworkPrefabs.Add(networkPrefab); + + foreach (var client in m_Clients) + { + client.NetworkConfig.NetworkPrefabs.Add(networkPrefab); + } + + yield return null; + } + + [UnityTearDown] + public IEnumerator Teardown() + { + // Shutdown and clean up both of our NetworkManager instances + if (m_ObjectToSpawn) + { + Object.Destroy(m_ObjectToSpawn); + m_ObjectToSpawn = null; + } + MultiInstanceHelpers.Destroy(); + yield return null; + } + + public enum InstanceType + { + Server, + Host, + Client + } + + /// + /// Tests that a spawned NetworkObject's associated NetworkBehaviours will have + /// their OnNetworkDespawn invoked during NetworkManager shutdown. + /// + [UnityTest] + public IEnumerator TestNetworkObjectDespawnOnShutdown([Values(InstanceType.Server, InstanceType.Host, InstanceType.Client)] InstanceType despawnCheck) + { + var useHost = despawnCheck == InstanceType.Server ? false : true; + var networkManager = despawnCheck == InstanceType.Host || despawnCheck == InstanceType.Server ? m_ServerHost : m_Clients[0]; + + // Start the instances + if (!MultiInstanceHelpers.Start(useHost, m_ServerHost, m_Clients)) + { + Debug.LogError("Failed to start instances"); + Assert.Fail("Failed to start instances"); + } + + // [Client-Side] Wait for a connection to the server + yield return MultiInstanceHelpers.WaitForClientsConnected(m_Clients, null, 512); + + // [Host-Server-Side] Check to make sure all clients are connected + var clientCount = useHost ? m_Clients.Length + 1 : m_Clients.Length; + yield return MultiInstanceHelpers.WaitForClientsConnectedToServer(m_ServerHost, clientCount, null, 512); + + // Spawn the test object + var spawnedObject = Object.Instantiate(m_NetworkObject); + var spawnedNetworkObject = spawnedObject.GetComponent(); + spawnedNetworkObject.NetworkManagerOwner = m_ServerHost; + spawnedNetworkObject.Spawn(true); + + // Get the spawned object relative to which NetworkManager instance we are testing. + var relativeSpawnedObject = new MultiInstanceHelpers.CoroutineResultWrapper(); + yield return MultiInstanceHelpers.Run(MultiInstanceHelpers.GetNetworkObjectByRepresentation((x => x.GetComponent() != null), networkManager, relativeSpawnedObject)); + var onNetworkDespawnTestComponent = relativeSpawnedObject.Result.GetComponent(); + + // Confirm it is not set before shutting down the NetworkManager + Assert.IsFalse(onNetworkDespawnTestComponent.OnNetworkDespawnCalled); + + // Shutdown the NetworkManager instance we are testing. + networkManager.Shutdown(); + + // Since shutdown is now delayed until the post frame update + // just wait 2 frames before checking to see if OnNetworkDespawnCalled is true + var currentFrame = Time.frameCount + 2; + yield return new WaitUntil(() => Time.frameCount <= currentFrame); + + // Confirm that OnNetworkDespawn is invoked after shutdown + Assert.IsTrue(onNetworkDespawnTestComponent.OnNetworkDespawnCalled); + } + } +} + diff --git a/Runtime/Collections/FixedQueue.cs.meta b/Tests/Runtime/NetworkObject/NetworkObjectOnNetworkDespawnTests.cs.meta similarity index 83% rename from Runtime/Collections/FixedQueue.cs.meta rename to Tests/Runtime/NetworkObject/NetworkObjectOnNetworkDespawnTests.cs.meta index bc855dd..895a567 100644 --- a/Runtime/Collections/FixedQueue.cs.meta +++ b/Tests/Runtime/NetworkObject/NetworkObjectOnNetworkDespawnTests.cs.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: a8514b4eca0c7044d9b92faf9407ec93 +guid: 93f8ca7aa8b616746a1c15592830b047 MonoImporter: externalObjects: {} serializedVersion: 2 diff --git a/Tests/Runtime/NetworkObject/NetworkObjectOwnershipTests.cs b/Tests/Runtime/NetworkObject/NetworkObjectOwnershipTests.cs index 26e2486..c5bdc8b 100644 --- a/Tests/Runtime/NetworkObject/NetworkObjectOwnershipTests.cs +++ b/Tests/Runtime/NetworkObject/NetworkObjectOwnershipTests.cs @@ -115,6 +115,9 @@ public IEnumerator TestOwnershipCallbacks() Assert.That(clientNetworkManager.SpawnManager.SpawnedObjects.ContainsKey(dummyNetworkObjectId)); } + // Verifies that removing the ownership when the default (server) is already set does not cause + // a Key Not Found Exception + m_ServerNetworkManager.SpawnManager.RemoveOwnership(dummyNetworkObject); var serverObject = m_ServerNetworkManager.SpawnManager.SpawnedObjects[dummyNetworkObjectId]; var clientObject = m_ClientNetworkManagers[0].SpawnManager.SpawnedObjects[dummyNetworkObjectId]; diff --git a/Tests/Runtime/NetworkVariableTests.cs b/Tests/Runtime/NetworkVariableTests.cs index 600883b..4325a7e 100644 --- a/Tests/Runtime/NetworkVariableTests.cs +++ b/Tests/Runtime/NetworkVariableTests.cs @@ -11,9 +11,19 @@ public struct TestStruct : INetworkSerializable { public uint SomeInt; public bool SomeBool; + public static bool NetworkSerializeCalledOnWrite; + public static bool NetworkSerializeCalledOnRead; public void NetworkSerialize(BufferSerializer serializer) where T : IReaderWriter { + if (serializer.IsReader) + { + NetworkSerializeCalledOnRead = true; + } + else + { + NetworkSerializeCalledOnWrite = true; + } serializer.SerializeValue(ref SomeInt); serializer.SerializeValue(ref SomeBool); } @@ -409,6 +419,28 @@ public IEnumerator TestNetworkVariableStruct([Values(true, false)] bool useHost) ); } + [UnityTest] + public IEnumerator TestINetworkSerializableCallsNetworkSerialize([Values(true, false)] bool useHost) + { + m_TestWithHost = useHost; + yield return MultiInstanceHelpers.RunAndWaitForCondition( + () => + { + TestStruct.NetworkSerializeCalledOnWrite = false; + TestStruct.NetworkSerializeCalledOnRead = false; + m_Player1OnServer.TheStruct.Value = + new TestStruct() { SomeInt = k_TestUInt, SomeBool = false }; + m_Player1OnServer.TheStruct.SetDirty(true); + }, + () => + { + return + TestStruct.NetworkSerializeCalledOnWrite && + TestStruct.NetworkSerializeCalledOnRead; + } + ); + } + [UnityTearDown] public override IEnumerator Teardown() { diff --git a/Tests/Runtime/Physics/NetworkRigidbody2DTest.cs b/Tests/Runtime/Physics/NetworkRigidbody2DTest.cs index 1a9f2a8..d053704 100644 --- a/Tests/Runtime/Physics/NetworkRigidbody2DTest.cs +++ b/Tests/Runtime/Physics/NetworkRigidbody2DTest.cs @@ -70,6 +70,7 @@ public IEnumerator TestRigidbodyKinematicEnableDisable() yield return NetworkRigidbodyTestBase.WaitForFrames(5); + // This should equal Kinematic Assert.IsTrue(serverPlayer.GetComponent().isKinematic == Kinematic); yield return NetworkRigidbodyTestBase.WaitForFrames(5); diff --git a/Tests/Runtime/RpcPipelineTestComponent.cs b/Tests/Runtime/RpcPipelineTestComponent.cs deleted file mode 100644 index 07e7fee..0000000 --- a/Tests/Runtime/RpcPipelineTestComponent.cs +++ /dev/null @@ -1,148 +0,0 @@ -using System; -using System.Collections.Generic; -using UnityEngine; - -namespace Unity.Netcode.RuntimeTests -{ - /// - /// Used in conjunction with the RpcQueueTest to validate: - /// - Sending and Receiving pipeline to validate that both sending and receiving pipelines are functioning properly. - /// - Usage of the ServerRpcParams.Send.UpdateStage and ClientRpcParams.Send.UpdateStage functionality. - /// - Rpcs receive will be invoked at the appropriate NetworkUpdateStage. - /// - public class RpcPipelineTestComponent : NetworkBehaviour - { - /// - /// Allows the external RPCQueueTest to begin testing or stop it - /// - public bool PingSelfEnabled; - - /// - /// How many times will we iterate through the various NetworkUpdateStage values? - /// (defaults to 2) - /// - public int MaxIterations = 2; - - // Start is called before the first frame update - private void Start() - { - m_MaxStagesSent = Enum.GetValues(typeof(NetworkUpdateStage)).Length * MaxIterations; - - //Start out with this being true (for first sequence) - m_ClientReceivedRpc = true; - } - - /// - /// Determine if we have iterated over more than our maximum stages we want to test - /// - /// true or false (did we exceed the max iterations or not?) - public bool ExceededMaxIterations() - { - if (m_StagesSent.Count > m_MaxStagesSent && m_MaxStagesSent > 0) - { - return true; - } - - return false; - } - - /// - /// Returns back whether the test has completed the total number of iterations - /// - /// - public bool IsTestComplete() - { - if (m_Counter >= MaxIterations) - { - return true; - } - - return false; - } - - private bool m_ClientReceivedRpc; - private int m_Counter = 0; - private int m_MaxStagesSent = 0; - private ServerRpcParams m_ServerParams; - private ClientRpcParams m_ClientParams; - private NetworkUpdateStage m_LastUpdateStage; - - // Update is called once per frame - private void Update() - { - if (NetworkManager.Singleton.IsListening && PingSelfEnabled && m_ClientReceivedRpc) - { - //Reset this for the next sequence of rpcs - m_ClientReceivedRpc = false; - - //As long as testing isn't completed, keep testing - if (!IsTestComplete()) - { - PingMySelfServerRpc(m_StagesSent.Count, m_ServerParams); - } - } - } - - - private readonly List m_ServerStagesReceived = new List(); - private readonly List m_ClientStagesReceived = new List(); - private readonly List m_StagesSent = new List(); - - /// - /// Assures all update stages were in alginment with one another - /// - /// true or false - public bool ValidateUpdateStages() - { - var validated = false; - if (m_ServerStagesReceived.Count == m_ClientStagesReceived.Count && m_ClientStagesReceived.Count == m_StagesSent.Count) - { - for (int i = 0; i < m_StagesSent.Count; i++) - { - var currentStage = m_StagesSent[i]; - if (m_ServerStagesReceived[i] != currentStage) - { - Debug.Log($"ServerRpc Update Stage ({m_ServerStagesReceived[i]}) is not equal to the current update stage ({currentStage})"); - - return validated; - } - - if (m_ClientStagesReceived[i] != currentStage) - { - Debug.Log($"ClientRpc Update Stage ({m_ClientStagesReceived[i]}) is not equal to the current update stage ({currentStage})"); - - return validated; - } - } - - validated = true; - } - - return validated; - } - - /// - /// Server side RPC for testing - /// - /// server rpc parameters - [ServerRpc] - private void PingMySelfServerRpc(int currentCount, ServerRpcParams parameters = default) - { - Debug.Log($"{nameof(PingMySelfServerRpc)}: [HostClient][ServerRpc][{currentCount}] invoked."); - - PingMySelfClientRpc(currentCount, m_ClientParams); - } - - /// - /// Client Side RPC called by PingMySelfServerRPC to validate both Client->Server and Server-Client pipeline is working - /// - /// client rpc parameters - [ClientRpc] - private void PingMySelfClientRpc(int currentCount, ClientRpcParams parameters = default) - { - Debug.Log($"{nameof(PingMySelfClientRpc)}: [HostServer][ClientRpc][{currentCount}] invoked. (previous output line should confirm this)"); - - m_ClientReceivedRpc = true; - } - } -} diff --git a/Tests/Runtime/RpcTests.cs b/Tests/Runtime/RpcTests.cs index 50ca4f6..2d6e96b 100644 --- a/Tests/Runtime/RpcTests.cs +++ b/Tests/Runtime/RpcTests.cs @@ -10,13 +10,13 @@ public class RpcTests : BaseMultiInstanceTest { public class RpcTestNB : NetworkBehaviour { - public event Action OnServer_Rpc; + public event Action OnServer_Rpc; public event Action OnClient_Rpc; [ServerRpc] - public void MyServerRpc() + public void MyServerRpc(ulong clientId, ServerRpcParams param = default) { - OnServer_Rpc(); + OnServer_Rpc(clientId, param); } [ClientRpc] @@ -42,11 +42,12 @@ public IEnumerator TestRpcs() { // This is the *SERVER VERSION* of the *CLIENT PLAYER* var serverClientPlayerResult = new MultiInstanceHelpers.CoroutineResultWrapper(); - yield return MultiInstanceHelpers.Run(MultiInstanceHelpers.GetNetworkObjectByRepresentation((x => x.IsPlayerObject && x.OwnerClientId == m_ClientNetworkManagers[0].LocalClientId), m_ServerNetworkManager, serverClientPlayerResult)); + var clientId = m_ClientNetworkManagers[0].LocalClientId; + yield return MultiInstanceHelpers.Run(MultiInstanceHelpers.GetNetworkObjectByRepresentation((x => x.IsPlayerObject && x.OwnerClientId == clientId), m_ServerNetworkManager, serverClientPlayerResult)); // This is the *CLIENT VERSION* of the *CLIENT PLAYER* var clientClientPlayerResult = new MultiInstanceHelpers.CoroutineResultWrapper(); - yield return MultiInstanceHelpers.Run(MultiInstanceHelpers.GetNetworkObjectByRepresentation((x => x.IsPlayerObject && x.OwnerClientId == m_ClientNetworkManagers[0].LocalClientId), m_ClientNetworkManagers[0], clientClientPlayerResult)); + yield return MultiInstanceHelpers.Run(MultiInstanceHelpers.GetNetworkObjectByRepresentation((x => x.IsPlayerObject && x.OwnerClientId == clientId), m_ClientNetworkManagers[0], clientClientPlayerResult)); // Setup state bool hasReceivedServerRpc = false; @@ -59,15 +60,16 @@ public IEnumerator TestRpcs() hasReceivedClientRpcRemotely = true; }; - clientClientPlayerResult.Result.GetComponent().OnServer_Rpc += () => + clientClientPlayerResult.Result.GetComponent().OnServer_Rpc += (clientId, param) => { // The RPC invoked locally. (Weaver failure?) Assert.Fail("ServerRpc invoked locally. Weaver failure?"); }; - serverClientPlayerResult.Result.GetComponent().OnServer_Rpc += () => + serverClientPlayerResult.Result.GetComponent().OnServer_Rpc += (clientId, param) => { Debug.Log("ServerRpc received on server object"); + Assert.True(param.Receive.SenderClientId == clientId); hasReceivedServerRpc = true; }; @@ -79,7 +81,7 @@ public IEnumerator TestRpcs() }; // Send ServerRpc - clientClientPlayerResult.Result.GetComponent().MyServerRpc(); + clientClientPlayerResult.Result.GetComponent().MyServerRpc(clientId); // Send ClientRpc serverClientPlayerResult.Result.GetComponent().MyClientRpc(); diff --git a/Tests/Runtime/StopStartRuntimeTests.cs b/Tests/Runtime/StopStartRuntimeTests.cs new file mode 100644 index 0000000..aa94fd9 --- /dev/null +++ b/Tests/Runtime/StopStartRuntimeTests.cs @@ -0,0 +1,73 @@ +using System.Collections; +using NUnit.Framework; +using UnityEngine; +using UnityEngine.TestTools; + +namespace Unity.Netcode.RuntimeTests +{ + public class StopStartRuntimeTests + { + [UnityTest] + public IEnumerator WhenShuttingDownAndRestarting_SDKRestartsSuccessfullyAndStaysRunning() + { // create server and client instances + MultiInstanceHelpers.Create(1, out NetworkManager server, out NetworkManager[] clients); + + try + { + + // create prefab + var gameObject = new GameObject("PlayerObject"); + var networkObject = gameObject.AddComponent(); + networkObject.DontDestroyWithOwner = true; + MultiInstanceHelpers.MakeNetworkObjectTestPrefab(networkObject); + + server.NetworkConfig.PlayerPrefab = gameObject; + + for (int i = 0; i < clients.Length; i++) + { + clients[i].NetworkConfig.PlayerPrefab = gameObject; + } + + // start server and connect clients + MultiInstanceHelpers.Start(false, server, clients); + + // wait for connection on client side + yield return MultiInstanceHelpers.Run(MultiInstanceHelpers.WaitForClientsConnected(clients)); + + // wait for connection on server side + yield return MultiInstanceHelpers.Run(MultiInstanceHelpers.WaitForClientConnectedToServer(server)); + + // shutdown the server + server.Shutdown(); + + // wait 1 frame because shutdowns are delayed + var nextFrameNumber = Time.frameCount + 1; + yield return new WaitUntil(() => Time.frameCount >= nextFrameNumber); + + // Verify the shutdown occurred + Assert.IsFalse(server.IsServer); + Assert.IsFalse(server.IsListening); + Assert.IsFalse(server.IsHost); + Assert.IsFalse(server.IsClient); + + server.StartServer(); + // Verify the server started + Assert.IsTrue(server.IsServer); + Assert.IsTrue(server.IsListening); + + // Wait several frames + nextFrameNumber = Time.frameCount + 10; + yield return new WaitUntil(() => Time.frameCount >= nextFrameNumber); + + // Verify the server is still running + Assert.IsTrue(server.IsServer); + Assert.IsTrue(server.IsListening); + } + finally + { + // cleanup + MultiInstanceHelpers.Destroy(); + } + } + } +} diff --git a/Tests/Runtime/RpcPipelineTestComponent.cs.meta b/Tests/Runtime/StopStartRuntimeTests.cs.meta similarity index 83% rename from Tests/Runtime/RpcPipelineTestComponent.cs.meta rename to Tests/Runtime/StopStartRuntimeTests.cs.meta index 2c945db..8df3e77 100644 --- a/Tests/Runtime/RpcPipelineTestComponent.cs.meta +++ b/Tests/Runtime/StopStartRuntimeTests.cs.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: e946a6fdcfcb9dd48b76b38871c0a77b +guid: 97a5298e33ee4d32be46ce84fecdcd06 MonoImporter: externalObjects: {} serializedVersion: 2 diff --git a/Tests/Runtime/Timing/TimeMultiInstanceTest.cs b/Tests/Runtime/Timing/TimeMultiInstanceTest.cs index c182b53..ccba585 100644 --- a/Tests/Runtime/Timing/TimeMultiInstanceTest.cs +++ b/Tests/Runtime/Timing/TimeMultiInstanceTest.cs @@ -56,8 +56,8 @@ public IEnumerator TestTimeMultiInstance(int targetFrameRate, uint tickRate) var networkManagers = MultiInstanceHelpers.NetworkManagerInstances.ToArray(); var server = networkManagers.First(t => t.IsServer); - var firstClient = networkManagers.First(t => t.IsClient); - var secondClient = networkManagers.Last(t => t.IsClient); + var firstClient = networkManagers.First(t => !t.IsServer); + var secondClient = networkManagers.Last(t => !t.IsServer); Assert.AreNotEqual(firstClient, secondClient); diff --git a/Tests/Runtime/Transport/SIPTransport.cs b/Tests/Runtime/Transport/SIPTransport.cs index 9ea8816..6dfa181 100644 --- a/Tests/Runtime/Transport/SIPTransport.cs +++ b/Tests/Runtime/Transport/SIPTransport.cs @@ -112,6 +112,7 @@ private void StopServer() { s_Server = null; m_Peers.Remove(ServerClientId); + m_LocalConnection = null; } public override void Shutdown() diff --git a/package.json b/package.json index a29572d..b858cf0 100644 --- a/package.json +++ b/package.json @@ -2,21 +2,22 @@ "name": "com.unity.netcode.gameobjects", "displayName": "Netcode for GameObjects", "description": "Netcode for GameObjects is a high-level netcode SDK that provides networking capabilities to GameObject/MonoBehaviour workflows within Unity and sits on top of underlying transport layer.", - "version": "1.0.0-pre.3", + "version": "1.0.0-pre.4", "unity": "2020.3", "dependencies": { - "com.unity.modules.ai": "1.0.0", "com.unity.modules.animation": "1.0.0", + "com.unity.modules.physics": "1.0.0", + "com.unity.modules.physics2d": "1.0.0", "com.unity.nuget.mono-cecil": "1.10.1", - "com.unity.collections": "1.0.0-pre.5" + "com.unity.collections": "1.1.0" }, "upmCi": { - "footprint": "883b3567bbb5155b7d06ef3c2cac755efa58a235" + "footprint": "28fd203207af71c78d6aaba679e94c5ead807529" }, "repository": { "url": "https://github.com/Unity-Technologies/com.unity.netcode.gameobjects.git", "type": "git", - "revision": "3e4df72dadeea8bd622da2824e30541910c79d3d" + "revision": "b14204cd059fb90f4a64319824105619f317d591" }, "samples": [ {