From cfa5789dfda508f5c299de2c95b002da04cc2851 Mon Sep 17 00:00:00 2001 From: Noel Stephens Date: Mon, 11 May 2026 09:44:52 -0500 Subject: [PATCH 01/26] fix: Improved OnValueChanged door example (#3959) * update Updating the door example to provide a more recommended way of using OnValueChanged. * style removing trailing spaces. * Update networkvariable.md Fixes to the example script * Update networkvariable.md Did another pass over this script with some improvements. * style removing trailing whitespace * Apply suggestions from code review Co-authored-by: Amy Reeve --------- Co-authored-by: Amy Reeve --- .../Documentation~/basics/networkvariable.md | 272 ++++++++++++++++-- 1 file changed, 253 insertions(+), 19 deletions(-) diff --git a/com.unity.netcode.gameobjects/Documentation~/basics/networkvariable.md b/com.unity.netcode.gameobjects/Documentation~/basics/networkvariable.md index 0ea6c445e8..df9b3bb506 100644 --- a/com.unity.netcode.gameobjects/Documentation~/basics/networkvariable.md +++ b/com.unity.netcode.gameobjects/Documentation~/basics/networkvariable.md @@ -148,43 +148,277 @@ The [synchronization and notification example](#synchronization-and-notification The `OnValueChanged` example shows a simple server-authoritative `NetworkVariable` being used to track the state of a door (open or closed) using an RPC that's sent to the server. Each time the door is used by a client, the `Door.ToggleStateRpc` is invoked and the server-side toggles the state of the door. When the `Door.State.Value` changes, all connected clients are synchronized to the (new) current `Value` and the `OnStateChanged` method is invoked locally on each client. ```csharp -public class Door : NetworkBehaviour +using System.Runtime.CompilerServices; +using Unity.Netcode; +using UnityEngine; + +/// +/// Example of using a to drive changes +/// in state. +/// +/// +/// This is a simple state driven door example. +/// This script was written with recommended usages patterns in mind. +/// +public class Door : NetworkBehaviour, INetworkUpdateSystem { - public NetworkVariable State = new NetworkVariable(); + /// + /// The two door states. + /// + public enum DoorStates + { + Closed, + Open + } + + /// + /// Initializes the door to a specific state (server side) when first spawned. + /// + [Tooltip("Configures the door's initial state when 1st spawned.")] + public DoorStates InitialState = DoorStates.Closed; + + /// + /// Used for example purposes. + /// When true, only the server can open and close the door. + /// Clients will receive a console log saying they could not open the door. + /// + public bool IsLocked; + + /// + /// A simple door state where the server has write permissions and everyone has read permissions. + /// + private NetworkVariable m_State = new NetworkVariable(default, NetworkVariableReadPermission.Everyone, NetworkVariableWritePermission.Server); + + /// + /// The current state of the door. + /// + public DoorStates CurrentState => m_State.Value; + /// + /// Invoked while the is in the process of + /// being spawned. + /// public override void OnNetworkSpawn() { - State.OnValueChanged += OnStateChanged; + // The write authority (server) doesn't need to know about its + // own changes (for this example) since it's the "single point + // of truth" for the door instance. + if (IsServer) + { + // Host/Server: + // Applies the configurable state upon spawning. + m_State.Value = InitialState; + } + else + { + // Clients: + // Subscribe to changes in the door's state. + m_State.OnValueChanged += OnStateChanged; + } } - public override void OnNetworkDespawn() + /// + /// Invoked once the door and all associated components + /// have finished the spawn process. + /// + protected override void OnNetworkPostSpawn() + { + // Everyone updates their door state when finished spawning the door + // to ensure the door reflects (visually) its current state. + UpdateFromState(); + + // Begin updating this NetworkBehaviour instance once all + // netcode related components have finished the spawn process. + NetworkUpdateLoop.RegisterNetworkUpdate(this, NetworkUpdateStage.Update); + base.OnNetworkPostSpawn(); + } + + /// + /// Example of using the usage pattern + /// where it only updates while spawned. + /// + /// The current update stage being invoked. + public void NetworkUpdate(NetworkUpdateStage updateStage) + { + switch (updateStage) + { + case NetworkUpdateStage.Update: + { + if (Input.GetKeyDown(KeyCode.Space)) + { + Interact(); + } + break; + } + } + } + + /// + /// Invoked just before this instance runs through its despawn + /// sequence. A good time to unsubscribe from things. + /// + public override void OnNetworkPreDespawn() + { + if (!IsServer) + { + m_State.OnValueChanged -= OnStateChanged; + } + + // Stop updating this NetworkBehaviour instance prior to running + // through the despawn process. + NetworkUpdateLoop.RegisterNetworkUpdate(this, NetworkUpdateStage.Update); + base.OnNetworkPreDespawn(); + } + + /// + /// Server makes changes to the state. + /// Clients receive the changes in state. + /// + /// + /// When the previous state equals the current state, we are a client + /// that is doing its first synchronization of this door instance. + /// + /// The previous state. + /// The current state. + public void OnStateChanged(DoorStates previous, DoorStates current) + { + UpdateFromState(); + } + + /// + /// Invoke when the state is updated to apply the change + /// in door state to the door asset itself. + /// + private void UpdateFromState() + { + switch(m_State.Value) + { + case DoorStates.Closed: + { + // door is open: + // - rotate door transform + // - play animations, sound etc. + /// + /// Override to apply specific checks (like a player having the right + /// key to open the door) or make it a non-virtual class and add logic + /// directly to this method. + /// + /// The player attempting to open the door. + /// + protected virtual bool CanPlayerToggleState(NetworkObject player) { - State.OnValueChanged -= OnStateChanged; + // For this example, if the door "is locked" then clients will + // not be able to open the door but the host-client's player can. + return !IsLocked || player.IsOwnedByServer; } - public void OnStateChanged(bool previous, bool current) + /// + /// Invoked by either a host or clients to interact with the door. + /// + public void Interact() { - // note: `State.Value` will be equal to `current` here - if (State.Value) + // Optional: + // This is only if you want clients to be able to + // interact with doors. A dedicated server would not + // be able to do this since it does not have a player. + if (IsServer && !IsHost) { - // door is open: - // - rotate door transform - // - play animations, sound etc. + // Optional to log a warning about this. + return; + } + + if (IsHost) + { + ToggleState(NetworkManager.LocalClientId); } else { - // door is closed: - // - rotate door transform - // - play animations, sound etc. + // Clients send an RPC to server (write authority) who applies the + // change in state that will be synchronized with all client observers. + ToggleStateRpc(); } } - [Rpc(SendTo.Server)] - public void ToggleStateRpc() + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private DoorStates NextToggleState() + { + return m_State.Value == DoorStates.Open ? DoorStates.Closed : DoorStates.Open; + } + + /// + /// Invoked only server-side + /// Primary method to handle toggling the door state. + /// + /// The client toggling the door state. + private void ToggleState(ulong clientId) + { + // Get the server-side client player instance + var playerObject = NetworkManager.SpawnManager.GetPlayerNetworkObject(clientId); + if (playerObject != null) + { + var nextToggleState = NextToggleState(); + if (CanPlayerToggleState(playerObject)) + { + // Host toggles the state + m_State.Value = nextToggleState; + UpdateFromState(); + } + else + { + ToggleStateFailRpc(nextToggleState, RpcTarget.Single(clientId, RpcTargetUse.Temp)); + } + } + else + { + // Optional as to how you handle this. Since ToggleState is only invoked by + // sever-side only script, this could mean many things depending upon whether + // or not a client could interact with something and not have a player object. + // If that is the case, then don't even bother checking for a player object. + // If that is not the case, then there could be a timing issue between when + // something can be "interacted with" and when a player is about to be de-spawned. + // For this example, we just log a warning as this example was built with + // the requirement that a client has a spawned player object that is used for + // reference to determine if the client's player can toggle the state of the + // door or not. + NetworkLog.LogWarningServer($"Client-{clientId} has no spawned player object!"); + } + } + + /// + /// Invoked by clients. + /// Re-directs to the common method. + /// + /// includes that is automatically populated for you. + [Rpc(SendTo.Server, InvokePermission = RpcInvokePermission.Everyone)] + private void ToggleStateRpc(RpcParams rpcParams = default) + { + ToggleState(rpcParams.Receive.SenderClientId); + } + + /// + /// Optional: + /// Handling when a player cannot open a door. + /// + /// includes that is automatically populated for you. + [Rpc(SendTo.SpecifiedInParams, InvokePermission = RpcInvokePermission.Server)] + private void ToggleStateFailRpc(DoorStates doorState, RpcParams rpcParams = default) { - // this will cause a replication over the network - // and ultimately invoke `OnValueChanged` on receivers - State.Value = !State.Value; + // Provide player feedback that toggling failed. + Debug.Log($"Failed to {doorState} the door!"); } } ``` From 885103818e85688a3477061f805fee0f686ca586 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Chrobot?= <124174716+michalChrobot@users.noreply.github.com> Date: Tue, 12 May 2026 12:22:12 +0200 Subject: [PATCH 02/26] ci: Update of CI dependencies and test fixes (#3970) * corrected Android issue * Updated dependencies * Updated XRAY_PROFILE param to account for latest checks --------- Co-authored-by: Noel Stephens Co-authored-by: Emma --- .yamato/package-pack.yml | 2 +- .yamato/package-tests.yml | 2 +- .yamato/project.metafile | 6 +- .yamato/wrench/api-validation-jobs.yml | 4 +- .yamato/wrench/package-pack-jobs.yml | 2 +- .yamato/wrench/preview-a-p-v.yml | 62 +++++++++---------- .yamato/wrench/promotion-jobs.yml | 9 +-- .yamato/wrench/recipe-regeneration.yml | 2 +- .yamato/wrench/validation-jobs.yml | 60 +++++++++--------- .yamato/wrench/wrench_config.json | 2 +- Tools/CI/NGO.Cookbook.csproj | 2 +- .../Runtime/Core/NetworkManager.cs | 2 +- 12 files changed, 78 insertions(+), 77 deletions(-) diff --git a/.yamato/package-pack.yml b/.yamato/package-pack.yml index 94c940b74b..7554274fe8 100644 --- a/.yamato/package-pack.yml +++ b/.yamato/package-pack.yml @@ -40,7 +40,7 @@ package_pack_-_ngo_{{ platform.name }}: {% endif %} timeout: 0.25 variables: - XRAY_PROFILE: "gold ./pvpExceptions.json" + XRAY_PROFILE: "gold ./pvpExceptions.json rme" commands: - upm-pvp pack "com.unity.netcode.gameobjects" --output upm-ci~/packages - upm-pvp xray --packages "upm-ci~/packages/com.unity.netcode.gameobjects*.tgz" --results pvp-results diff --git a/.yamato/package-tests.yml b/.yamato/package-tests.yml index e9df795f10..50799533dd 100644 --- a/.yamato/package-tests.yml +++ b/.yamato/package-tests.yml @@ -36,7 +36,7 @@ package_test_-_ngo_{{ editor }}_{{ platform.name }}: model: {{ platform.model }} # This is set only in platforms where we want non-default model to use (more information in project.metafile) {% endif %} variables: - XRAY_PROFILE: "gold ./pvpExceptions.json" + XRAY_PROFILE: "gold ./pvpExceptions.json rme" UNITY_EXT_LOGGING: 1 commands: - unity-downloader-cli --fast --wait -u {{ editor }} -c Editor {% if platform.name == "mac" %} --arch arm64 {% endif %} # For macOS we use ARM64 models. diff --git a/.yamato/project.metafile b/.yamato/project.metafile index 871ff18710..acc0c1d405 100644 --- a/.yamato/project.metafile +++ b/.yamato/project.metafile @@ -24,7 +24,7 @@ small_agent_platform: - name: ubuntu type: Unity::VM - image: package-ci/ubuntu-22.04:v4.83.0 + image: package-ci/ubuntu-22.04:v4.85.0 flavor: b1.small @@ -39,13 +39,13 @@ test_platforms: default: - name: ubuntu type: Unity::VM - image: package-ci/ubuntu-22.04:v4.83.0 + image: package-ci/ubuntu-22.04:v4.85.0 flavor: b1.large standalone: StandaloneLinux64 desktop: - name: ubuntu type: Unity::VM - image: package-ci/ubuntu-22.04:v4.83.0 + image: package-ci/ubuntu-22.04:v4.85.0 flavor: b1.large smaller_flavor: b1.medium larger_flavor: b1.xlarge diff --git a/.yamato/wrench/api-validation-jobs.yml b/.yamato/wrench/api-validation-jobs.yml index d99abd06f0..6976c10973 100644 --- a/.yamato/wrench/api-validation-jobs.yml +++ b/.yamato/wrench/api-validation-jobs.yml @@ -60,8 +60,8 @@ api_validation_-_netcode_gameobjects_-_6000_0_-_win10: UNITY_LICENSING_SERVER_DELETE_NUL: 0 UNITY_LICENSING_SERVER_DELETE_ULF: 0 UNITY_LICENSING_SERVER_TOOLSET: pro - UPMPVP_CONTEXT_WRENCH: 2.7.0.0 + UPMPVP_CONTEXT_WRENCH: 2.9.0.0 metadata: Job Maintainers: '#rm-packageworks' - Wrench: 2.7.0.0 + Wrench: 2.9.0.0 diff --git a/.yamato/wrench/package-pack-jobs.yml b/.yamato/wrench/package-pack-jobs.yml index 3994a564c2..06c750a127 100644 --- a/.yamato/wrench/package-pack-jobs.yml +++ b/.yamato/wrench/package-pack-jobs.yml @@ -29,5 +29,5 @@ package_pack_-_netcode_gameobjects: UPMCI_ACK_LARGE_PACKAGE: 1 metadata: Job Maintainers: '#rm-packageworks' - Wrench: 2.7.0.0 + Wrench: 2.9.0.0 diff --git a/.yamato/wrench/preview-a-p-v.yml b/.yamato/wrench/preview-a-p-v.yml index 55736dc6b8..445f507a73 100644 --- a/.yamato/wrench/preview-a-p-v.yml +++ b/.yamato/wrench/preview-a-p-v.yml @@ -27,7 +27,7 @@ all_preview_apv_jobs: - path: .yamato/wrench/preview-a-p-v.yml#preview_apv_-_6000_6_-_win10 metadata: Job Maintainers: '#rm-packageworks' - Wrench: 2.7.0.0 + Wrench: 2.9.0.0 # Functional tests for dependents found in the latest 6000.0 manifest (MacOS). preview_apv_-_6000_0_-_macos13: @@ -82,10 +82,10 @@ preview_apv_-_6000_0_-_macos13: UNITY_LICENSING_SERVER_DELETE_NUL: 0 UNITY_LICENSING_SERVER_DELETE_ULF: 0 UNITY_LICENSING_SERVER_TOOLSET: pro - UPMPVP_CONTEXT_WRENCH: 2.7.0.0 + UPMPVP_CONTEXT_WRENCH: 2.9.0.0 metadata: Job Maintainers: '#rm-packageworks' - Wrench: 2.7.0.0 + Wrench: 2.9.0.0 # Functional tests for dependents found in the latest 6000.0 manifest (Ubuntu). preview_apv_-_6000_0_-_ubuntu2204: @@ -140,10 +140,10 @@ preview_apv_-_6000_0_-_ubuntu2204: UNITY_LICENSING_SERVER_DELETE_NUL: 0 UNITY_LICENSING_SERVER_DELETE_ULF: 0 UNITY_LICENSING_SERVER_TOOLSET: pro - UPMPVP_CONTEXT_WRENCH: 2.7.0.0 + UPMPVP_CONTEXT_WRENCH: 2.9.0.0 metadata: Job Maintainers: '#rm-packageworks' - Wrench: 2.7.0.0 + Wrench: 2.9.0.0 # Functional tests for dependents found in the latest 6000.0 manifest (Windows). preview_apv_-_6000_0_-_win10: @@ -199,10 +199,10 @@ preview_apv_-_6000_0_-_win10: UNITY_LICENSING_SERVER_DELETE_NUL: 0 UNITY_LICENSING_SERVER_DELETE_ULF: 0 UNITY_LICENSING_SERVER_TOOLSET: pro - UPMPVP_CONTEXT_WRENCH: 2.7.0.0 + UPMPVP_CONTEXT_WRENCH: 2.9.0.0 metadata: Job Maintainers: '#rm-packageworks' - Wrench: 2.7.0.0 + Wrench: 2.9.0.0 # Functional tests for dependents found in the latest 6000.3 manifest (MacOS). preview_apv_-_6000_3_-_macos13: @@ -257,10 +257,10 @@ preview_apv_-_6000_3_-_macos13: UNITY_LICENSING_SERVER_DELETE_NUL: 0 UNITY_LICENSING_SERVER_DELETE_ULF: 0 UNITY_LICENSING_SERVER_TOOLSET: pro - UPMPVP_CONTEXT_WRENCH: 2.7.0.0 + UPMPVP_CONTEXT_WRENCH: 2.9.0.0 metadata: Job Maintainers: '#rm-packageworks' - Wrench: 2.7.0.0 + Wrench: 2.9.0.0 # Functional tests for dependents found in the latest 6000.3 manifest (Ubuntu). preview_apv_-_6000_3_-_ubuntu2204: @@ -315,10 +315,10 @@ preview_apv_-_6000_3_-_ubuntu2204: UNITY_LICENSING_SERVER_DELETE_NUL: 0 UNITY_LICENSING_SERVER_DELETE_ULF: 0 UNITY_LICENSING_SERVER_TOOLSET: pro - UPMPVP_CONTEXT_WRENCH: 2.7.0.0 + UPMPVP_CONTEXT_WRENCH: 2.9.0.0 metadata: Job Maintainers: '#rm-packageworks' - Wrench: 2.7.0.0 + Wrench: 2.9.0.0 # Functional tests for dependents found in the latest 6000.3 manifest (Windows). preview_apv_-_6000_3_-_win10: @@ -374,10 +374,10 @@ preview_apv_-_6000_3_-_win10: UNITY_LICENSING_SERVER_DELETE_NUL: 0 UNITY_LICENSING_SERVER_DELETE_ULF: 0 UNITY_LICENSING_SERVER_TOOLSET: pro - UPMPVP_CONTEXT_WRENCH: 2.7.0.0 + UPMPVP_CONTEXT_WRENCH: 2.9.0.0 metadata: Job Maintainers: '#rm-packageworks' - Wrench: 2.7.0.0 + Wrench: 2.9.0.0 # Functional tests for dependents found in the latest 6000.4 manifest (MacOS). preview_apv_-_6000_4_-_macos13: @@ -432,10 +432,10 @@ preview_apv_-_6000_4_-_macos13: UNITY_LICENSING_SERVER_DELETE_NUL: 0 UNITY_LICENSING_SERVER_DELETE_ULF: 0 UNITY_LICENSING_SERVER_TOOLSET: pro - UPMPVP_CONTEXT_WRENCH: 2.7.0.0 + UPMPVP_CONTEXT_WRENCH: 2.9.0.0 metadata: Job Maintainers: '#rm-packageworks' - Wrench: 2.7.0.0 + Wrench: 2.9.0.0 # Functional tests for dependents found in the latest 6000.4 manifest (Ubuntu). preview_apv_-_6000_4_-_ubuntu2204: @@ -490,10 +490,10 @@ preview_apv_-_6000_4_-_ubuntu2204: UNITY_LICENSING_SERVER_DELETE_NUL: 0 UNITY_LICENSING_SERVER_DELETE_ULF: 0 UNITY_LICENSING_SERVER_TOOLSET: pro - UPMPVP_CONTEXT_WRENCH: 2.7.0.0 + UPMPVP_CONTEXT_WRENCH: 2.9.0.0 metadata: Job Maintainers: '#rm-packageworks' - Wrench: 2.7.0.0 + Wrench: 2.9.0.0 # Functional tests for dependents found in the latest 6000.4 manifest (Windows). preview_apv_-_6000_4_-_win10: @@ -549,10 +549,10 @@ preview_apv_-_6000_4_-_win10: UNITY_LICENSING_SERVER_DELETE_NUL: 0 UNITY_LICENSING_SERVER_DELETE_ULF: 0 UNITY_LICENSING_SERVER_TOOLSET: pro - UPMPVP_CONTEXT_WRENCH: 2.7.0.0 + UPMPVP_CONTEXT_WRENCH: 2.9.0.0 metadata: Job Maintainers: '#rm-packageworks' - Wrench: 2.7.0.0 + Wrench: 2.9.0.0 # Functional tests for dependents found in the latest 6000.5 manifest (MacOS). preview_apv_-_6000_5_-_macos13: @@ -607,10 +607,10 @@ preview_apv_-_6000_5_-_macos13: UNITY_LICENSING_SERVER_DELETE_NUL: 0 UNITY_LICENSING_SERVER_DELETE_ULF: 0 UNITY_LICENSING_SERVER_TOOLSET: pro - UPMPVP_CONTEXT_WRENCH: 2.7.0.0 + UPMPVP_CONTEXT_WRENCH: 2.9.0.0 metadata: Job Maintainers: '#rm-packageworks' - Wrench: 2.7.0.0 + Wrench: 2.9.0.0 # Functional tests for dependents found in the latest 6000.5 manifest (Ubuntu). preview_apv_-_6000_5_-_ubuntu2204: @@ -665,10 +665,10 @@ preview_apv_-_6000_5_-_ubuntu2204: UNITY_LICENSING_SERVER_DELETE_NUL: 0 UNITY_LICENSING_SERVER_DELETE_ULF: 0 UNITY_LICENSING_SERVER_TOOLSET: pro - UPMPVP_CONTEXT_WRENCH: 2.7.0.0 + UPMPVP_CONTEXT_WRENCH: 2.9.0.0 metadata: Job Maintainers: '#rm-packageworks' - Wrench: 2.7.0.0 + Wrench: 2.9.0.0 # Functional tests for dependents found in the latest 6000.5 manifest (Windows). preview_apv_-_6000_5_-_win10: @@ -724,10 +724,10 @@ preview_apv_-_6000_5_-_win10: UNITY_LICENSING_SERVER_DELETE_NUL: 0 UNITY_LICENSING_SERVER_DELETE_ULF: 0 UNITY_LICENSING_SERVER_TOOLSET: pro - UPMPVP_CONTEXT_WRENCH: 2.7.0.0 + UPMPVP_CONTEXT_WRENCH: 2.9.0.0 metadata: Job Maintainers: '#rm-packageworks' - Wrench: 2.7.0.0 + Wrench: 2.9.0.0 # Functional tests for dependents found in the latest 6000.6 manifest (MacOS). preview_apv_-_6000_6_-_macos13: @@ -782,10 +782,10 @@ preview_apv_-_6000_6_-_macos13: UNITY_LICENSING_SERVER_DELETE_NUL: 0 UNITY_LICENSING_SERVER_DELETE_ULF: 0 UNITY_LICENSING_SERVER_TOOLSET: pro - UPMPVP_CONTEXT_WRENCH: 2.7.0.0 + UPMPVP_CONTEXT_WRENCH: 2.9.0.0 metadata: Job Maintainers: '#rm-packageworks' - Wrench: 2.7.0.0 + Wrench: 2.9.0.0 # Functional tests for dependents found in the latest 6000.6 manifest (Ubuntu). preview_apv_-_6000_6_-_ubuntu2204: @@ -840,10 +840,10 @@ preview_apv_-_6000_6_-_ubuntu2204: UNITY_LICENSING_SERVER_DELETE_NUL: 0 UNITY_LICENSING_SERVER_DELETE_ULF: 0 UNITY_LICENSING_SERVER_TOOLSET: pro - UPMPVP_CONTEXT_WRENCH: 2.7.0.0 + UPMPVP_CONTEXT_WRENCH: 2.9.0.0 metadata: Job Maintainers: '#rm-packageworks' - Wrench: 2.7.0.0 + Wrench: 2.9.0.0 # Functional tests for dependents found in the latest 6000.6 manifest (Windows). preview_apv_-_6000_6_-_win10: @@ -899,8 +899,8 @@ preview_apv_-_6000_6_-_win10: UNITY_LICENSING_SERVER_DELETE_NUL: 0 UNITY_LICENSING_SERVER_DELETE_ULF: 0 UNITY_LICENSING_SERVER_TOOLSET: pro - UPMPVP_CONTEXT_WRENCH: 2.7.0.0 + UPMPVP_CONTEXT_WRENCH: 2.9.0.0 metadata: Job Maintainers: '#rm-packageworks' - Wrench: 2.7.0.0 + Wrench: 2.9.0.0 diff --git a/.yamato/wrench/promotion-jobs.yml b/.yamato/wrench/promotion-jobs.yml index e4beeec200..7fd22a018d 100644 --- a/.yamato/wrench/promotion-jobs.yml +++ b/.yamato/wrench/promotion-jobs.yml @@ -183,10 +183,10 @@ publish_dry_run_netcode_gameobjects: unzip: true variables: UPMPVP_ACK_UPMPVP_DOES_NO_API_VALIDATION: 1 - UPMPVP_CONTEXT_WRENCH: 2.7.0.0 + UPMPVP_CONTEXT_WRENCH: 2.9.0.0 metadata: Job Maintainers: '#rm-packageworks' - Wrench: 2.7.0.0 + Wrench: 2.9.0.0 # Publish for netcode.gameobjects to https://artifactory-slo.bf.unity3d.com/artifactory/api/npm/upm-npm publish_netcode_gameobjects: @@ -365,8 +365,9 @@ publish_netcode_gameobjects: unzip: true variables: UPMPVP_ACK_UPMPVP_DOES_NO_API_VALIDATION: 1 - UPMPVP_CONTEXT_WRENCH: 2.7.0.0 + UPMPVP_CONTEXT_WRENCH: 2.9.0.0 + allow_on: branch match "^release/.*" metadata: Job Maintainers: '#rm-packageworks' - Wrench: 2.7.0.0 + Wrench: 2.9.0.0 diff --git a/.yamato/wrench/recipe-regeneration.yml b/.yamato/wrench/recipe-regeneration.yml index 1550408dd2..b9eba06e54 100644 --- a/.yamato/wrench/recipe-regeneration.yml +++ b/.yamato/wrench/recipe-regeneration.yml @@ -31,5 +31,5 @@ test_-_wrench_jobs_up_to_date: cancel_old_ci: true metadata: Job Maintainers: '#rm-packageworks' - Wrench: 2.7.0.0 + Wrench: 2.9.0.0 diff --git a/.yamato/wrench/validation-jobs.yml b/.yamato/wrench/validation-jobs.yml index f247f7d6a4..84a92fcd75 100644 --- a/.yamato/wrench/validation-jobs.yml +++ b/.yamato/wrench/validation-jobs.yml @@ -69,10 +69,10 @@ validate_-_netcode_gameobjects_-_6000_0_-_macos13: UNITY_LICENSING_SERVER_DELETE_ULF: 0 UNITY_LICENSING_SERVER_TOOLSET: pro UPMPVP_ACK_UPMPVP_DOES_NO_API_VALIDATION: 1 - UPMPVP_CONTEXT_WRENCH: 2.7.0.0 + UPMPVP_CONTEXT_WRENCH: 2.9.0.0 metadata: Job Maintainers: '#rm-packageworks' - Wrench: 2.7.0.0 + Wrench: 2.9.0.0 labels: - Packages:netcode.gameobjects @@ -139,10 +139,10 @@ validate_-_netcode_gameobjects_-_6000_0_-_ubuntu2204: UNITY_LICENSING_SERVER_DELETE_ULF: 0 UNITY_LICENSING_SERVER_TOOLSET: pro UPMPVP_ACK_UPMPVP_DOES_NO_API_VALIDATION: 1 - UPMPVP_CONTEXT_WRENCH: 2.7.0.0 + UPMPVP_CONTEXT_WRENCH: 2.9.0.0 metadata: Job Maintainers: '#rm-packageworks' - Wrench: 2.7.0.0 + Wrench: 2.9.0.0 labels: - Packages:netcode.gameobjects @@ -209,10 +209,10 @@ validate_-_netcode_gameobjects_-_6000_0_-_win10: UNITY_LICENSING_SERVER_DELETE_ULF: 0 UNITY_LICENSING_SERVER_TOOLSET: pro UPMPVP_ACK_UPMPVP_DOES_NO_API_VALIDATION: 1 - UPMPVP_CONTEXT_WRENCH: 2.7.0.0 + UPMPVP_CONTEXT_WRENCH: 2.9.0.0 metadata: Job Maintainers: '#rm-packageworks' - Wrench: 2.7.0.0 + Wrench: 2.9.0.0 labels: - Packages:netcode.gameobjects @@ -279,10 +279,10 @@ validate_-_netcode_gameobjects_-_6000_3_-_macos13: UNITY_LICENSING_SERVER_DELETE_ULF: 0 UNITY_LICENSING_SERVER_TOOLSET: pro UPMPVP_ACK_UPMPVP_DOES_NO_API_VALIDATION: 1 - UPMPVP_CONTEXT_WRENCH: 2.7.0.0 + UPMPVP_CONTEXT_WRENCH: 2.9.0.0 metadata: Job Maintainers: '#rm-packageworks' - Wrench: 2.7.0.0 + Wrench: 2.9.0.0 labels: - Packages:netcode.gameobjects @@ -349,10 +349,10 @@ validate_-_netcode_gameobjects_-_6000_3_-_ubuntu2204: UNITY_LICENSING_SERVER_DELETE_ULF: 0 UNITY_LICENSING_SERVER_TOOLSET: pro UPMPVP_ACK_UPMPVP_DOES_NO_API_VALIDATION: 1 - UPMPVP_CONTEXT_WRENCH: 2.7.0.0 + UPMPVP_CONTEXT_WRENCH: 2.9.0.0 metadata: Job Maintainers: '#rm-packageworks' - Wrench: 2.7.0.0 + Wrench: 2.9.0.0 labels: - Packages:netcode.gameobjects @@ -419,10 +419,10 @@ validate_-_netcode_gameobjects_-_6000_3_-_win10: UNITY_LICENSING_SERVER_DELETE_ULF: 0 UNITY_LICENSING_SERVER_TOOLSET: pro UPMPVP_ACK_UPMPVP_DOES_NO_API_VALIDATION: 1 - UPMPVP_CONTEXT_WRENCH: 2.7.0.0 + UPMPVP_CONTEXT_WRENCH: 2.9.0.0 metadata: Job Maintainers: '#rm-packageworks' - Wrench: 2.7.0.0 + Wrench: 2.9.0.0 labels: - Packages:netcode.gameobjects @@ -489,10 +489,10 @@ validate_-_netcode_gameobjects_-_6000_4_-_macos13: UNITY_LICENSING_SERVER_DELETE_ULF: 0 UNITY_LICENSING_SERVER_TOOLSET: pro UPMPVP_ACK_UPMPVP_DOES_NO_API_VALIDATION: 1 - UPMPVP_CONTEXT_WRENCH: 2.7.0.0 + UPMPVP_CONTEXT_WRENCH: 2.9.0.0 metadata: Job Maintainers: '#rm-packageworks' - Wrench: 2.7.0.0 + Wrench: 2.9.0.0 labels: - Packages:netcode.gameobjects @@ -559,10 +559,10 @@ validate_-_netcode_gameobjects_-_6000_4_-_ubuntu2204: UNITY_LICENSING_SERVER_DELETE_ULF: 0 UNITY_LICENSING_SERVER_TOOLSET: pro UPMPVP_ACK_UPMPVP_DOES_NO_API_VALIDATION: 1 - UPMPVP_CONTEXT_WRENCH: 2.7.0.0 + UPMPVP_CONTEXT_WRENCH: 2.9.0.0 metadata: Job Maintainers: '#rm-packageworks' - Wrench: 2.7.0.0 + Wrench: 2.9.0.0 labels: - Packages:netcode.gameobjects @@ -629,10 +629,10 @@ validate_-_netcode_gameobjects_-_6000_4_-_win10: UNITY_LICENSING_SERVER_DELETE_ULF: 0 UNITY_LICENSING_SERVER_TOOLSET: pro UPMPVP_ACK_UPMPVP_DOES_NO_API_VALIDATION: 1 - UPMPVP_CONTEXT_WRENCH: 2.7.0.0 + UPMPVP_CONTEXT_WRENCH: 2.9.0.0 metadata: Job Maintainers: '#rm-packageworks' - Wrench: 2.7.0.0 + Wrench: 2.9.0.0 labels: - Packages:netcode.gameobjects @@ -699,10 +699,10 @@ validate_-_netcode_gameobjects_-_6000_5_-_macos13: UNITY_LICENSING_SERVER_DELETE_ULF: 0 UNITY_LICENSING_SERVER_TOOLSET: pro UPMPVP_ACK_UPMPVP_DOES_NO_API_VALIDATION: 1 - UPMPVP_CONTEXT_WRENCH: 2.7.0.0 + UPMPVP_CONTEXT_WRENCH: 2.9.0.0 metadata: Job Maintainers: '#rm-packageworks' - Wrench: 2.7.0.0 + Wrench: 2.9.0.0 labels: - Packages:netcode.gameobjects @@ -769,10 +769,10 @@ validate_-_netcode_gameobjects_-_6000_5_-_ubuntu2204: UNITY_LICENSING_SERVER_DELETE_ULF: 0 UNITY_LICENSING_SERVER_TOOLSET: pro UPMPVP_ACK_UPMPVP_DOES_NO_API_VALIDATION: 1 - UPMPVP_CONTEXT_WRENCH: 2.7.0.0 + UPMPVP_CONTEXT_WRENCH: 2.9.0.0 metadata: Job Maintainers: '#rm-packageworks' - Wrench: 2.7.0.0 + Wrench: 2.9.0.0 labels: - Packages:netcode.gameobjects @@ -839,10 +839,10 @@ validate_-_netcode_gameobjects_-_6000_5_-_win10: UNITY_LICENSING_SERVER_DELETE_ULF: 0 UNITY_LICENSING_SERVER_TOOLSET: pro UPMPVP_ACK_UPMPVP_DOES_NO_API_VALIDATION: 1 - UPMPVP_CONTEXT_WRENCH: 2.7.0.0 + UPMPVP_CONTEXT_WRENCH: 2.9.0.0 metadata: Job Maintainers: '#rm-packageworks' - Wrench: 2.7.0.0 + Wrench: 2.9.0.0 labels: - Packages:netcode.gameobjects @@ -909,10 +909,10 @@ validate_-_netcode_gameobjects_-_6000_6_-_macos13: UNITY_LICENSING_SERVER_DELETE_ULF: 0 UNITY_LICENSING_SERVER_TOOLSET: pro UPMPVP_ACK_UPMPVP_DOES_NO_API_VALIDATION: 1 - UPMPVP_CONTEXT_WRENCH: 2.7.0.0 + UPMPVP_CONTEXT_WRENCH: 2.9.0.0 metadata: Job Maintainers: '#rm-packageworks' - Wrench: 2.7.0.0 + Wrench: 2.9.0.0 labels: - Packages:netcode.gameobjects @@ -979,10 +979,10 @@ validate_-_netcode_gameobjects_-_6000_6_-_ubuntu2204: UNITY_LICENSING_SERVER_DELETE_ULF: 0 UNITY_LICENSING_SERVER_TOOLSET: pro UPMPVP_ACK_UPMPVP_DOES_NO_API_VALIDATION: 1 - UPMPVP_CONTEXT_WRENCH: 2.7.0.0 + UPMPVP_CONTEXT_WRENCH: 2.9.0.0 metadata: Job Maintainers: '#rm-packageworks' - Wrench: 2.7.0.0 + Wrench: 2.9.0.0 labels: - Packages:netcode.gameobjects @@ -1049,10 +1049,10 @@ validate_-_netcode_gameobjects_-_6000_6_-_win10: UNITY_LICENSING_SERVER_DELETE_ULF: 0 UNITY_LICENSING_SERVER_TOOLSET: pro UPMPVP_ACK_UPMPVP_DOES_NO_API_VALIDATION: 1 - UPMPVP_CONTEXT_WRENCH: 2.7.0.0 + UPMPVP_CONTEXT_WRENCH: 2.9.0.0 metadata: Job Maintainers: '#rm-packageworks' - Wrench: 2.7.0.0 + Wrench: 2.9.0.0 labels: - Packages:netcode.gameobjects diff --git a/.yamato/wrench/wrench_config.json b/.yamato/wrench/wrench_config.json index 2d7eb092b2..9aa8ecfde1 100644 --- a/.yamato/wrench/wrench_config.json +++ b/.yamato/wrench/wrench_config.json @@ -39,7 +39,7 @@ }, "publishing_job": ".yamato/wrench/promotion-jobs.yml#publish_netcode_gameobjects", "branch_pattern": "ReleaseSlash", - "wrench_version": "2.7.0.0", + "wrench_version": "2.9.0.0", "pvp_exemption_path": ".yamato/wrench/pvp-exemptions.json", "cs_project_path": "Tools/CI/NGO.Cookbook.csproj" } \ No newline at end of file diff --git a/Tools/CI/NGO.Cookbook.csproj b/Tools/CI/NGO.Cookbook.csproj index aa420f1382..452a22131a 100644 --- a/Tools/CI/NGO.Cookbook.csproj +++ b/Tools/CI/NGO.Cookbook.csproj @@ -12,7 +12,7 @@ - + diff --git a/com.unity.netcode.gameobjects/Runtime/Core/NetworkManager.cs b/com.unity.netcode.gameobjects/Runtime/Core/NetworkManager.cs index b9a2c15789..8fc97dd84e 100644 --- a/com.unity.netcode.gameobjects/Runtime/Core/NetworkManager.cs +++ b/com.unity.netcode.gameobjects/Runtime/Core/NetworkManager.cs @@ -1013,7 +1013,7 @@ internal bool NetworkManagerCheckForParent(bool ignoreNetworkManagerCache = fals var isParented = transform.root != transform; if (isParented) { - throw new Exception(GenerateNestedNetworkManagerMessage(transform)); + Log.Error(new Context(LogLevel.Error, GenerateNestedNetworkManagerMessage(transform))); } #endif return isParented; From d6e96d6f6bb36a8ff695722ed87addf9528ebb85 Mon Sep 17 00:00:00 2001 From: Noel Stephens Date: Wed, 13 May 2026 14:58:41 -0500 Subject: [PATCH 03/26] fix: UniversalRpcTests failing when using IL2CPP backend (#3973) fix Fixing the issue with `UniversalRpcTests` failing on IL2CPP release builds using the "Method Name" stack trace information setting. --- .../Tests/Runtime/Rpc/UniversalRpcTests.cs | 67 ++++++++++++++----- 1 file changed, 52 insertions(+), 15 deletions(-) diff --git a/com.unity.netcode.gameobjects/Tests/Runtime/Rpc/UniversalRpcTests.cs b/com.unity.netcode.gameobjects/Tests/Runtime/Rpc/UniversalRpcTests.cs index f943422b39..fb90660844 100644 --- a/com.unity.netcode.gameobjects/Tests/Runtime/Rpc/UniversalRpcTests.cs +++ b/com.unity.netcode.gameobjects/Tests/Runtime/Rpc/UniversalRpcTests.cs @@ -2,9 +2,9 @@ using System; using System.Collections; using System.Collections.Generic; -using System.Diagnostics; using System.Linq; using System.Reflection; +using System.Runtime.CompilerServices; using NUnit.Framework; using Unity.Collections; using Unity.Netcode.TestHelpers.Runtime; @@ -29,22 +29,14 @@ internal class UniversalRpcNetworkBehaviour : NetworkBehaviour public ulong ReceivedFrom = ulong.MaxValue; public int ReceivedCount; - public void OnRpcReceived() + public void OnRpcReceived([CallerMemberName] string methodName = "") { - var st = new StackTrace(); - var sf = st.GetFrame(1); - - var currentMethod = sf.GetMethod(); - Received = currentMethod.Name; + Received = methodName; ReceivedCount++; } - public void OnRpcReceivedWithParams(int a, bool b, float f, string s) + public void OnRpcReceivedWithParams(int a, bool b, float f, string s, [CallerMemberName] string methodName = "") { - var st = new StackTrace(); - var sf = st.GetFrame(1); - - var currentMethod = sf.GetMethod(); - Received = currentMethod.Name; + Received = methodName; ReceivedCount++; ReceivedParams = new Tuple(a, b, f, s); } @@ -1031,7 +1023,8 @@ public void VerifySentToNotServerWithParams(ulong objectOwner, ulong sender, str public void VerifySentToClientsAndHostWithParams(ulong objectOwner, ulong sender, string methodName, int i, bool b, float f, string s) { - if (m_ServerNetworkManager.IsHost) + var authority = GetAuthorityNetworkManager(); + if (authority.IsHost) { VerifySentToEveryoneWithParams(objectOwner, sender, methodName, i, b, f, s); } @@ -1319,11 +1312,33 @@ private void SendingNoOverrideWithParams(SendTo sendTo, ulong objectOwner, ulong var sendMethodName = $"DefaultTo{sendTo}WithParamsRpc"; var verifyMethodName = $"VerifySentTo{sendTo}WithParams"; + VerboseDebug("Get player object..."); var senderObject = GetPlayerObject(objectOwner, sender); + if (senderObject == null) + { + Debug.LogError($"Sender object is NULL!"); + Assert.Fail(); + return; + } + + VerboseDebug($"Getting send method: {sendMethodName}"); var sendMethod = senderObject.GetType().GetMethod(sendMethodName); + if (sendMethod == null) + { + Debug.LogError($"Send method for {sendMethodName} is NULL!"); + Assert.Fail(); + return; + } sendMethod.Invoke(senderObject, new object[] { i, b, f, s }); + VerboseDebug($"Getting verify method: {verifyMethodName}"); var verifyMethod = GetType().GetMethod(verifyMethodName); + if (verifyMethod == null) + { + Debug.LogError($"Verify method for {verifyMethodName} is NULL!"); + Assert.Fail(); + return; + } verifyMethod.Invoke(this, new object[] { objectOwner, sender, sendMethodName, i, b, f, s }); } @@ -1344,11 +1359,33 @@ private void SendingNoOverrideWithParamsAndRpcParams(SendTo sendTo, ulong object var sendMethodName = $"DefaultTo{sendTo}WithParamsAndRpcParamsRpc"; var verifyMethodName = $"VerifySentTo{sendTo}WithParams"; + VerboseDebug("Get player object..."); var senderObject = GetPlayerObject(objectOwner, sender); + if (senderObject == null) + { + Debug.LogError($"Sender object is NULL!"); + Assert.Fail(); + return; + } + + VerboseDebug($"Getting send method: {sendMethodName}"); var sendMethod = senderObject.GetType().GetMethod(sendMethodName); + if (sendMethod == null) + { + Debug.LogError($"Send method for {sendMethodName} is NULL!"); + Assert.Fail(); + return; + } sendMethod.Invoke(senderObject, new object[] { i, b, f, s, new RpcParams() }); + VerboseDebug($"Getting verify method: {verifyMethodName}"); var verifyMethod = GetType().GetMethod(verifyMethodName); + if (verifyMethod == null) + { + Debug.LogError($"Verify method for {verifyMethodName} is NULL!"); + Assert.Fail(); + return; + } verifyMethod.Invoke(this, new object[] { objectOwner, sender, sendMethodName, i, b, f, s }); } @@ -1383,7 +1420,7 @@ private void RunTestTypeB(TestTypes testType) { foreach (var defaultSendTo in sendToValues) { - UnityEngine.Debug.Log($"[{testType}][{defaultSendTo}]"); + VerboseDebug($"[{testType}][{defaultSendTo}]"); foreach (var overrideSendTo in sendToValues) { for (ulong objectOwner = 0; objectOwner <= numberOfClientsULong; objectOwner++) From 294364316d20141cf53a2b2afed1293574014865 Mon Sep 17 00:00:00 2001 From: Simon Lemay Date: Thu, 14 May 2026 11:34:09 -0400 Subject: [PATCH 04/26] feat: Make custom driver construction smoother (#3980) * feat: Make custom driver construction smoother * Add PR number to CHANGELOG entries * Update package version --- com.unity.netcode.gameobjects/CHANGELOG.md | 2 ++ .../UTP/INetworkStreamDriverConstructor.cs | 3 +-- .../UTP/NetworkMetricsPipelineStage.cs | 4 ---- .../Runtime/Transports/UTP/UnityTransport.cs | 22 +++++++++++++++---- com.unity.netcode.gameobjects/package.json | 4 ++-- 5 files changed, 23 insertions(+), 12 deletions(-) diff --git a/com.unity.netcode.gameobjects/CHANGELOG.md b/com.unity.netcode.gameobjects/CHANGELOG.md index 9d1075d0b3..0d8fb3a2fb 100644 --- a/com.unity.netcode.gameobjects/CHANGELOG.md +++ b/com.unity.netcode.gameobjects/CHANGELOG.md @@ -10,9 +10,11 @@ Additional documentation and release notes are available at [Multiplayer Documen ### Added +- Added a new variant of `UnityTransport.GetDefaultPipelineConfigurations` that takes a reference to the created `NetworkDriver`. This will register all pipeline stages that `UnityTransport` requires, removing the need to manually register them in your own custom driver constructor. (#3980) ### Changed +- `NetworkMetricsPipelineStage` is now defined even when the multiplayer tools package is not installed, removing the need to guard its registration behind a version define when using a custom driver in `UnityTransport`. (#3980) ### Deprecated diff --git a/com.unity.netcode.gameobjects/Runtime/Transports/UTP/INetworkStreamDriverConstructor.cs b/com.unity.netcode.gameobjects/Runtime/Transports/UTP/INetworkStreamDriverConstructor.cs index 8e07c0d3e4..c07c8c1484 100644 --- a/com.unity.netcode.gameobjects/Runtime/Transports/UTP/INetworkStreamDriverConstructor.cs +++ b/com.unity.netcode.gameobjects/Runtime/Transports/UTP/INetworkStreamDriverConstructor.cs @@ -36,9 +36,8 @@ namespace Unity.Netcode.Transports.UTP /// var settings = transport.GetDefaultNetworkSettings(); /// driver = NetworkDriver.Create(new IPCNetworkInterface(), settings); /// - /// driver.RegisterPipelineStage(new NetworkMetricsPipelineStage()); - /// /// transport.GetDefaultPipelineConfigurations( + /// ref driver, /// out var unreliableFragmentedPipelineStages, /// out var unreliableSequencedFragmentedPipelineStages, /// out var reliableSequencedPipelineStages); diff --git a/com.unity.netcode.gameobjects/Runtime/Transports/UTP/NetworkMetricsPipelineStage.cs b/com.unity.netcode.gameobjects/Runtime/Transports/UTP/NetworkMetricsPipelineStage.cs index c602aef739..de1e7899b9 100644 --- a/com.unity.netcode.gameobjects/Runtime/Transports/UTP/NetworkMetricsPipelineStage.cs +++ b/com.unity.netcode.gameobjects/Runtime/Transports/UTP/NetworkMetricsPipelineStage.cs @@ -1,5 +1,3 @@ -#if MULTIPLAYER_TOOLS -#if MULTIPLAYER_TOOLS_1_0_0_PRE_7 using AOT; using Unity.Burst; using Unity.Collections.LowLevel.Unsafe; @@ -70,5 +68,3 @@ private static void InitializeConnection(byte* staticInstanceBuffer, int staticI } } } -#endif -#endif diff --git a/com.unity.netcode.gameobjects/Runtime/Transports/UTP/UnityTransport.cs b/com.unity.netcode.gameobjects/Runtime/Transports/UTP/UnityTransport.cs index b39fe2bc94..e392af403c 100644 --- a/com.unity.netcode.gameobjects/Runtime/Transports/UTP/UnityTransport.cs +++ b/com.unity.netcode.gameobjects/Runtime/Transports/UTP/UnityTransport.cs @@ -657,6 +657,23 @@ public void GetDefaultPipelineConfigurations( reliableSequencedPipelineStages = new(reliableSequenced, Allocator.Temp); } + /// + /// Driver for which the pipeline configurations are being retrieved. + public void GetDefaultPipelineConfigurations( + ref NetworkDriver driver, + out NativeArray unreliableFragmentedPipelineStages, + out NativeArray unreliableSequencedFragmentedPipelineStages, + out NativeArray reliableSequencedPipelineStages) + { +#if MULTIPLAYER_TOOLS_1_0_0_PRE_7 + driver.RegisterPipelineStage(new NetworkMetricsPipelineStage()); +#endif + GetDefaultPipelineConfigurations( + out unreliableFragmentedPipelineStages, + out unreliableSequencedFragmentedPipelineStages, + out reliableSequencedPipelineStages); + } + private NetworkPipeline SelectSendPipeline(NetworkDelivery delivery) { switch (delivery) @@ -1860,11 +1877,8 @@ public void CreateDriver( #endif } -#if MULTIPLAYER_TOOLS_1_0_0_PRE_7 - driver.RegisterPipelineStage(new NetworkMetricsPipelineStage()); -#endif - GetDefaultPipelineConfigurations( + ref driver, out var unreliableFragmentedPipelineStages, out var unreliableSequencedFragmentedPipelineStages, out var reliableSequencedPipelineStages); diff --git a/com.unity.netcode.gameobjects/package.json b/com.unity.netcode.gameobjects/package.json index 04492deaa0..bd77e48697 100644 --- a/com.unity.netcode.gameobjects/package.json +++ b/com.unity.netcode.gameobjects/package.json @@ -2,7 +2,7 @@ "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": "2.11.3", + "version": "2.12.0", "unity": "6000.0", "dependencies": { "com.unity.nuget.mono-cecil": "1.11.4", @@ -15,4 +15,4 @@ "path": "Samples~/Bootstrap" } ] -} \ No newline at end of file +} From 5d6a3c736e149d2ac3cc67c17df06af5ea937279 Mon Sep 17 00:00:00 2001 From: Simon Lemay Date: Fri, 15 May 2026 11:57:30 -0400 Subject: [PATCH 05/26] chore: Change codeowners for UTP (#3982) --- .github/CODEOWNERS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index d54be2e86e..ad9486f7b4 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -10,4 +10,4 @@ AssemblyInfo.cs @NoelStephensUnity @EmandM @Unity-Technologies/netcode-qa .github/ @NoelStephensUnity @EmandM @Unity-Technologies/netcode-qa .yamato/ @NoelStephensUnity @EmandM @Unity-Technologies/netcode-qa com.unity.netcode.gameobjects/Documentation*/ @jabbacakes -com.unity.netcode.gameobjects/Runtime/Transports/UTP/ @Unity-Technologies/multiplayer-workflows +com.unity.netcode.gameobjects/Runtime/Transports/UTP/ @simon-lemay-unity From 75c36f439be444d0b1b4780f2d66113d5f58a8a6 Mon Sep 17 00:00:00 2001 From: Emma Date: Thu, 21 May 2026 15:54:37 -0400 Subject: [PATCH 06/26] chore: Add obsolete warnings to unused or invalid methods (#3987) * chore: Add obsolete warnings to unused or invalid methods * Update CHANGELOG * dotnet-fix * remove duplication of GetRootParentTransform * Remove unrelated change * Leave the public modifiers in place * Update xml doc --- com.unity.netcode.gameobjects/CHANGELOG.md | 1 + .../Editor/CodeGen/RuntimeAccessModifiersILPP.cs | 8 ++++++++ .../Editor/NetworkBehaviourEditor.cs | 15 +++++---------- .../Runtime/Core/NetworkManager.cs | 3 +++ .../NetworkVariable/Collections/NetworkList.cs | 10 ++-------- .../Runtime/Spawning/NetworkSpawnManager.cs | 1 + .../Manual/NestedNetworkTransforms/ChildMover.cs | 11 +---------- .../Assets/Tests/Runtime/RpcTestsAutomated.cs | 11 +++++------ 8 files changed, 26 insertions(+), 34 deletions(-) diff --git a/com.unity.netcode.gameobjects/CHANGELOG.md b/com.unity.netcode.gameobjects/CHANGELOG.md index 0d8fb3a2fb..06ebf2c108 100644 --- a/com.unity.netcode.gameobjects/CHANGELOG.md +++ b/com.unity.netcode.gameobjects/CHANGELOG.md @@ -18,6 +18,7 @@ Additional documentation and release notes are available at [Multiplayer Documen ### Deprecated +- Deprecated a number of methods that were no longer valid or being used. (#3987) ### Removed diff --git a/com.unity.netcode.gameobjects/Editor/CodeGen/RuntimeAccessModifiersILPP.cs b/com.unity.netcode.gameobjects/Editor/CodeGen/RuntimeAccessModifiersILPP.cs index 57295be52e..ffea9115b7 100644 --- a/com.unity.netcode.gameobjects/Editor/CodeGen/RuntimeAccessModifiersILPP.cs +++ b/com.unity.netcode.gameobjects/Editor/CodeGen/RuntimeAccessModifiersILPP.cs @@ -97,17 +97,23 @@ private void ProcessNetworkManager(TypeDefinition typeDefinition, string[] assem { foreach (var fieldDefinition in typeDefinition.Fields) { +#pragma warning disable CS0618 // Type or member is obsolete if (fieldDefinition.Name == nameof(NetworkManager.__rpc_func_table)) +#pragma warning restore CS0618 // Type or member is obsolete { fieldDefinition.IsPublic = true; } +#pragma warning disable CS0618 // Type or member is obsolete if (fieldDefinition.Name == nameof(NetworkManager.RpcReceiveHandler)) +#pragma warning restore CS0618 // Type or member is obsolete { fieldDefinition.IsPublic = true; } +#pragma warning disable CS0618 // Type or member is obsolete if (fieldDefinition.Name == nameof(NetworkManager.__rpc_name_table)) +#pragma warning restore CS0618 // Type or member is obsolete { fieldDefinition.IsPublic = true; } @@ -115,7 +121,9 @@ private void ProcessNetworkManager(TypeDefinition typeDefinition, string[] assem foreach (var nestedTypeDefinition in typeDefinition.NestedTypes) { +#pragma warning disable CS0618 // Type or member is obsolete if (nestedTypeDefinition.Name == nameof(NetworkManager.RpcReceiveHandler)) +#pragma warning restore CS0618 // Type or member is obsolete { nestedTypeDefinition.IsNestedPublic = true; } diff --git a/com.unity.netcode.gameobjects/Editor/NetworkBehaviourEditor.cs b/com.unity.netcode.gameobjects/Editor/NetworkBehaviourEditor.cs index 1312ff69e6..c2ce7da626 100644 --- a/com.unity.netcode.gameobjects/Editor/NetworkBehaviourEditor.cs +++ b/com.unity.netcode.gameobjects/Editor/NetworkBehaviourEditor.cs @@ -322,18 +322,13 @@ private void OnEnable() } /// - /// Recursively finds the root parent of a + /// Obsolete method to find root parent of a . + /// Use instead. /// /// The current we are inspecting for a parent /// the root parent for the first passed into the method - public static Transform GetRootParentTransform(Transform transform) - { - if (transform.parent == null || transform.parent == transform) - { - return transform; - } - return GetRootParentTransform(transform.parent); - } + [Obsolete("Use transform.root instead")] + public static Transform GetRootParentTransform(Transform transform) => transform.root; /// /// Used to determine if a GameObject has one or more NetworkBehaviours but @@ -358,7 +353,7 @@ public static void CheckForNetworkObject(GameObject gameObject, bool networkObje } // Now get the root parent transform to the current GameObject (or itself) - var rootTransform = GetRootParentTransform(gameObject.transform); + var rootTransform = gameObject.transform.root; if (!rootTransform.TryGetComponent(out var networkManager)) { networkManager = rootTransform.GetComponentInChildren(); diff --git a/com.unity.netcode.gameobjects/Runtime/Core/NetworkManager.cs b/com.unity.netcode.gameobjects/Runtime/Core/NetworkManager.cs index 8fc97dd84e..55fdd6dbf8 100644 --- a/com.unity.netcode.gameobjects/Runtime/Core/NetworkManager.cs +++ b/com.unity.netcode.gameobjects/Runtime/Core/NetworkManager.cs @@ -44,12 +44,15 @@ public class NetworkManager : MonoBehaviour, INetworkUpdateSystem #pragma warning disable IDE1006 // disable naming rule violation check // RuntimeAccessModifiersILPP will make this `public` + [Obsolete("This field is no longer used and will be removed in a future version.")] internal delegate void RpcReceiveHandler(NetworkBehaviour behaviour, FastBufferReader reader, __RpcParams parameters); // RuntimeAccessModifiersILPP will make this `public` + [Obsolete("This field is no longer used and will be removed in a future version.")] internal static readonly Dictionary __rpc_func_table = new Dictionary(); // RuntimeAccessModifiersILPP will make this `public` (legacy table should be removed in v3.x.x) + [Obsolete("This field is no longer used and will be removed in a future version.")] internal static readonly Dictionary __rpc_name_table = new Dictionary(); #pragma warning restore IDE1006 // restore naming rule violation check diff --git a/com.unity.netcode.gameobjects/Runtime/NetworkVariable/Collections/NetworkList.cs b/com.unity.netcode.gameobjects/Runtime/NetworkVariable/Collections/NetworkList.cs index aafeeb2ab4..9c0cb4bd83 100644 --- a/com.unity.netcode.gameobjects/Runtime/NetworkVariable/Collections/NetworkList.cs +++ b/com.unity.netcode.gameobjects/Runtime/NetworkVariable/Collections/NetworkList.cs @@ -683,14 +683,8 @@ private void HandleAddListEvent(NetworkListEvent listEvent) /// /// This method should not be used. It is left over from a previous interface /// - public int LastModifiedTick - { - get - { - // todo: implement proper network tick for NetworkList - return NetworkTickSystem.NoTick; - } - } + [Obsolete("This property is no longer used and will be removed in a future version.")] + public int LastModifiedTick => NetworkTickSystem.NoTick; /// /// Overridden implementation. diff --git a/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkSpawnManager.cs b/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkSpawnManager.cs index 52e891f8e1..7a2f45b1d1 100644 --- a/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkSpawnManager.cs +++ b/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkSpawnManager.cs @@ -448,6 +448,7 @@ private bool TryGetNetworkClient(ulong clientId, out NetworkClient networkClient /// /// not used /// not used + [Obsolete("This method is no longer used and will be removed in a future version.")] protected virtual void InternalOnOwnershipChanged(ulong perviousOwner, ulong newOwner) { diff --git a/testproject/Assets/Tests/Manual/NestedNetworkTransforms/ChildMover.cs b/testproject/Assets/Tests/Manual/NestedNetworkTransforms/ChildMover.cs index f731d6fe95..efaceb4a38 100644 --- a/testproject/Assets/Tests/Manual/NestedNetworkTransforms/ChildMover.cs +++ b/testproject/Assets/Tests/Manual/NestedNetworkTransforms/ChildMover.cs @@ -45,20 +45,11 @@ public void PlayerIsMoving(float movementDirection) } } - private Transform GetRootParentTransform(Transform transform) - { - if (transform.parent != null) - { - return GetRootParentTransform(transform.parent); - } - return transform; - } - protected override void OnNetworkPostSpawn() { if (CanCommitToTransform) { - m_RootParentTransform = GetRootParentTransform(transform); + m_RootParentTransform = transform.root; if (RandomizeScale) { transform.localScale = transform.localScale * Random.Range(0.5f, 1.5f); diff --git a/testproject/Assets/Tests/Runtime/RpcTestsAutomated.cs b/testproject/Assets/Tests/Runtime/RpcTestsAutomated.cs index 228bfbfdd5..1332aed20a 100644 --- a/testproject/Assets/Tests/Runtime/RpcTestsAutomated.cs +++ b/testproject/Assets/Tests/Runtime/RpcTestsAutomated.cs @@ -6,7 +6,6 @@ using Unity.Netcode.TestHelpers.Runtime; using UnityEngine; using UnityEngine.TestTools; -using Debug = UnityEngine.Debug; namespace TestProject.RuntimeTests { @@ -120,16 +119,16 @@ private IEnumerator AutomatedRpcTestsHandler(int numClients) Assert.IsFalse(m_TimedOut); // Log the output for visual confirmation (Acceptance Test for this test) that all RPC test types (tracked by counters) executed multiple times - Debug.Log("Final Host-Server Status Info:"); - Debug.Log(serverRpcTests.GetCurrentServerStatusInfo()); + VerboseDebug("Final Host-Server Status Info:"); + VerboseDebug(serverRpcTests.GetCurrentServerStatusInfo()); foreach (var rpcClientSideTest in clientRpcQueueManualTestInstsances) { - Debug.Log($"Final Client {rpcClientSideTest.NetworkManager.LocalClientId} Status Info:"); - Debug.Log(rpcClientSideTest.GetCurrentClientStatusInfo()); + VerboseDebug($"Final Client {rpcClientSideTest.NetworkManager.LocalClientId} Status Info:"); + VerboseDebug(rpcClientSideTest.GetCurrentClientStatusInfo()); } - Debug.Log($"Total frames updated = {Time.frameCount - startFrameCount} within {Time.realtimeSinceStartup - startTime} seconds."); + VerboseDebug($"Total frames updated = {Time.frameCount - startFrameCount} within {Time.realtimeSinceStartup - startTime} seconds."); } } } From 61417401ce4506288b115f22e8f3fc75f9e5b456 Mon Sep 17 00:00:00 2001 From: Netcode team bot Date: Wed, 27 May 2026 16:20:58 +0200 Subject: [PATCH 07/26] chore: Updated aspects of Netcode package in anticipation of v2.12.0 release (#3998) --- com.unity.netcode.gameobjects/CHANGELOG.md | 17 ++++++++++++++--- com.unity.netcode.gameobjects/package.json | 4 ++-- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/com.unity.netcode.gameobjects/CHANGELOG.md b/com.unity.netcode.gameobjects/CHANGELOG.md index 06ebf2c108..4a31c4ff55 100644 --- a/com.unity.netcode.gameobjects/CHANGELOG.md +++ b/com.unity.netcode.gameobjects/CHANGELOG.md @@ -10,15 +10,12 @@ Additional documentation and release notes are available at [Multiplayer Documen ### Added -- Added a new variant of `UnityTransport.GetDefaultPipelineConfigurations` that takes a reference to the created `NetworkDriver`. This will register all pipeline stages that `UnityTransport` requires, removing the need to manually register them in your own custom driver constructor. (#3980) ### Changed -- `NetworkMetricsPipelineStage` is now defined even when the multiplayer tools package is not installed, removing the need to guard its registration behind a version define when using a custom driver in `UnityTransport`. (#3980) ### Deprecated -- Deprecated a number of methods that were no longer valid or being used. (#3987) ### Removed @@ -32,6 +29,20 @@ Additional documentation and release notes are available at [Multiplayer Documen ### Obsolete +## [2.12.0] - 2026-05-24 + +### Added + +- Added a new variant of `UnityTransport.GetDefaultPipelineConfigurations` that takes a reference to the created `NetworkDriver`. This will register all pipeline stages that `UnityTransport` requires, removing the need to manually register them in your own custom driver constructor. (#3980) + +### Changed + +- `NetworkMetricsPipelineStage` is now defined even when the multiplayer tools package is not installed, removing the need to guard its registration behind a version define when using a custom driver in `UnityTransport`. (#3980) + +### Deprecated + +- Deprecated a number of methods that were no longer valid or being used. (#3987) + ### [2.11.2] - 2026-05-01 ### Fixed diff --git a/com.unity.netcode.gameobjects/package.json b/com.unity.netcode.gameobjects/package.json index bd77e48697..5400c16d26 100644 --- a/com.unity.netcode.gameobjects/package.json +++ b/com.unity.netcode.gameobjects/package.json @@ -2,7 +2,7 @@ "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": "2.12.0", + "version": "2.12.1", "unity": "6000.0", "dependencies": { "com.unity.nuget.mono-cecil": "1.11.4", @@ -15,4 +15,4 @@ "path": "Samples~/Bootstrap" } ] -} +} \ No newline at end of file From 7c992c261b8692e70581de93f02b9e10506061fa Mon Sep 17 00:00:00 2001 From: Emma Date: Fri, 29 May 2026 13:55:36 -0400 Subject: [PATCH 08/26] chore: Replace IsSceneObject with InScenePlaced bool (#4000) * chore: Replace IsSceneObject with InScenePlaced bool * Add XML doc * Update small files * Fix findobjects complaints * Fix tests * Turn off logs * ignore invalid scenes on the scene manager --- com.unity.netcode.gameobjects/CHANGELOG.md | 1 + .../Editor/InScenePlacedProcessor.cs | 45 +++++++++ .../Editor/InScenePlacedProcessor.cs.meta | 2 + .../Editor/NetworkObjectEditor.cs | 12 +-- .../Runtime/Components/NetworkTransform.cs | 2 +- .../Connection/NetworkConnectionManager.cs | 6 +- .../Runtime/Core/FindObjects.cs | 94 ++++++++++++++++++- .../Runtime/Core/NetworkManager.cs | 2 +- .../Runtime/Core/NetworkObject.cs | 47 ++++++---- .../DefaultSceneManagerHandler.cs | 2 +- .../SceneManagement/NetworkSceneHandle.cs | 6 +- .../SceneManagement/NetworkSceneManager.cs | 31 +++--- .../Runtime/SceneManagement/SceneEventData.cs | 48 +++++----- .../Runtime/Spawning/NetworkSpawnManager.cs | 69 ++++++-------- .../NetworkObjectOnSpawnTests.cs | 1 - .../NetworkObjectSpawnManyObjectsTests.cs | 1 - ...orkVariableBaseInitializesWhenPersisted.cs | 2 - .../Prefabs/NetworkPrefabOverrideTests.cs | 5 - .../IntegrationTestSceneHandler.cs | 50 +++------- .../TestHelpers/NetcodeIntegrationTest.cs | 1 - .../NetcodeIntegrationTestHelpers.cs | 3 - com.unity.netcode.gameobjects/package.json | 2 +- .../Tests/Runtime/NetworkManagerTests.cs | 2 +- .../AttachableBehaviourSceneLoadTests.cs | 2 +- .../ClientSynchronizationModeTests.cs | 61 +++++++++++- .../ClientSynchronizationValidationTest.cs | 4 +- .../NetworkSceneManagerDDOLTests.cs | 12 +-- .../NetworkSceneManagerEventDataPoolTest.cs | 6 +- ...NetworkSceneManagerPopulateInSceneTests.cs | 4 +- .../ParentDynamicUnderInScenePlaced.cs | 10 +- .../Tests/Runtime/PrefabExtendedTests.cs | 2 +- 31 files changed, 345 insertions(+), 190 deletions(-) create mode 100644 com.unity.netcode.gameobjects/Editor/InScenePlacedProcessor.cs create mode 100644 com.unity.netcode.gameobjects/Editor/InScenePlacedProcessor.cs.meta diff --git a/com.unity.netcode.gameobjects/CHANGELOG.md b/com.unity.netcode.gameobjects/CHANGELOG.md index 4a31c4ff55..186d442d32 100644 --- a/com.unity.netcode.gameobjects/CHANGELOG.md +++ b/com.unity.netcode.gameobjects/CHANGELOG.md @@ -16,6 +16,7 @@ Additional documentation and release notes are available at [Multiplayer Documen ### Deprecated +- Deprecated the nullable boolean `NetworkObject.IsSceneObject` and introduced `NetworkObject.InScenePlaced`. (#4000) ### Removed diff --git a/com.unity.netcode.gameobjects/Editor/InScenePlacedProcessor.cs b/com.unity.netcode.gameobjects/Editor/InScenePlacedProcessor.cs new file mode 100644 index 0000000000..423ff4a65c --- /dev/null +++ b/com.unity.netcode.gameobjects/Editor/InScenePlacedProcessor.cs @@ -0,0 +1,45 @@ +using UnityEditor; +using UnityEditor.Build; +using UnityEditor.Build.Reporting; +using UnityEngine; +using UnityEngine.SceneManagement; + +namespace Unity.Netcode.Editor +{ + /// + /// A that sets the property to true for all s in the scene. + /// Ensures that InScenePlaced is always true for all objects in the scene. + /// + /// + /// This will always run as the game enters the scene, + /// + internal class SetInScenePlaced : IProcessSceneWithReport + { + public int callbackOrder => 0; + public void OnProcessScene(Scene scene, BuildReport report) + { + foreach (var networkObject in FindObjects.FromSceneByType(scene, true)) + { + networkObject.InScenePlaced = true; + } + } + } + + /// + /// An that sets the property to false for all s in prefabs. + /// Ensures that InScenePlaced is always false for all prefab objects. + /// This is important because when a prefab is instantiated in the scene, it should be treated as a dynamically spawned object. + /// + internal class InScenePlacedPrefabBuilder : AssetPostprocessor + { + public void OnPostprocessPrefab(GameObject root) + { + var networkObjects = root.GetComponentsInChildren(true); + foreach (var networkObject in networkObjects) + { + networkObject.InScenePlaced = false; + } + } + } +} diff --git a/com.unity.netcode.gameobjects/Editor/InScenePlacedProcessor.cs.meta b/com.unity.netcode.gameobjects/Editor/InScenePlacedProcessor.cs.meta new file mode 100644 index 0000000000..784aa38575 --- /dev/null +++ b/com.unity.netcode.gameobjects/Editor/InScenePlacedProcessor.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: cda0fb70bdcd545a7983ca78f516bcff \ No newline at end of file diff --git a/com.unity.netcode.gameobjects/Editor/NetworkObjectEditor.cs b/com.unity.netcode.gameobjects/Editor/NetworkObjectEditor.cs index 04e0d1f698..0335896784 100644 --- a/com.unity.netcode.gameobjects/Editor/NetworkObjectEditor.cs +++ b/com.unity.netcode.gameobjects/Editor/NetworkObjectEditor.cs @@ -67,14 +67,10 @@ public override void OnInspectorGUI() EditorGUILayout.Toggle(nameof(NetworkObject.IsOwner), m_NetworkObject.IsOwner); EditorGUILayout.Toggle(nameof(NetworkObject.IsOwnedByServer), m_NetworkObject.IsOwnedByServer); EditorGUILayout.Toggle(nameof(NetworkObject.IsPlayerObject), m_NetworkObject.IsPlayerObject); - if (m_NetworkObject.IsSceneObject.HasValue) - { - EditorGUILayout.Toggle(nameof(NetworkObject.IsSceneObject), m_NetworkObject.IsSceneObject.Value); - } - else - { - EditorGUILayout.TextField(nameof(NetworkObject.IsSceneObject), "null"); - } +#pragma warning disable CS0618 // Type or member is obsolete + // TODO-3.x: Update name in 3.x branch + EditorGUILayout.Toggle(nameof(NetworkObject.IsSceneObject), m_NetworkObject.InScenePlaced); +#pragma warning restore CS0618 // Type or member is obsolete EditorGUILayout.Toggle(nameof(NetworkObject.DestroyWithScene), m_NetworkObject.DestroyWithScene); EditorGUILayout.TextField(nameof(NetworkObject.NetworkManager), m_NetworkObject.NetworkManager == null ? "null" : m_NetworkObject.NetworkManager.gameObject.name); GUI.enabled = guiEnabled; diff --git a/com.unity.netcode.gameobjects/Runtime/Components/NetworkTransform.cs b/com.unity.netcode.gameobjects/Runtime/Components/NetworkTransform.cs index 3ca9093b34..0259138b46 100644 --- a/com.unity.netcode.gameobjects/Runtime/Components/NetworkTransform.cs +++ b/com.unity.netcode.gameobjects/Runtime/Components/NetworkTransform.cs @@ -2231,7 +2231,7 @@ private bool CheckForStateChange(ref NetworkTransformState networkState, bool is // In-scene placed NetworkObjects parented under a GameObject with no // NetworkObject preserve their lossyScale when synchronizing. - if (parentNetworkObject == null && NetworkObject.IsSceneObject != false) + if (parentNetworkObject == null && NetworkObject.InScenePlaced) { hasParentNetworkObject = true; } diff --git a/com.unity.netcode.gameobjects/Runtime/Connection/NetworkConnectionManager.cs b/com.unity.netcode.gameobjects/Runtime/Connection/NetworkConnectionManager.cs index aaeb67f4db..1da9ddb3bb 100644 --- a/com.unity.netcode.gameobjects/Runtime/Connection/NetworkConnectionManager.cs +++ b/com.unity.netcode.gameobjects/Runtime/Connection/NetworkConnectionManager.cs @@ -1184,7 +1184,11 @@ internal void CreateAndSpawnPlayer(ulong ownerId) return; } - networkObject.IsSceneObject = false; +#pragma warning disable CS0618 // Type or member is obsolete + // Obsolete with warning means we need the underlying behaviour to keep existing + // TODO: remove in the 3.x branch + networkObject.SetSceneObjectStatus(false); +#pragma warning restore CS0618 // Type or member is obsolete networkObject.NetworkManagerOwner = NetworkManager; networkObject.SpawnAsPlayerObject(ownerId, networkObject.DestroyWithScene); } diff --git a/com.unity.netcode.gameobjects/Runtime/Core/FindObjects.cs b/com.unity.netcode.gameobjects/Runtime/Core/FindObjects.cs index a283cabe52..59329f8a81 100644 --- a/com.unity.netcode.gameobjects/Runtime/Core/FindObjects.cs +++ b/com.unity.netcode.gameobjects/Runtime/Core/FindObjects.cs @@ -1,7 +1,8 @@ -#if NGO_FINDOBJECTS_NOSORTING using System; -#endif +using System.Collections; +using System.Collections.Generic; using System.Runtime.CompilerServices; +using UnityEngine.SceneManagement; using Object = UnityEngine.Object; namespace Unity.Netcode @@ -11,14 +12,14 @@ namespace Unity.Netcode /// /// /// It is intentional that we do not include the UnityEngine namespace in order to avoid - /// over-complicatd define wrapping between versions that do or don't support FindObjectsSortMode. + /// over-complicated define wrapping between versions that do or don't support FindObjectsSortMode. /// internal static class FindObjects { /// /// Replaces to have one place where these changes are applied. /// - /// + /// The type of object to find. Must be a reference type derived from /// When true, inactive objects will be included. /// When true, the array returned will be sorted by identifier. /// Results as an of type T @@ -39,5 +40,90 @@ public static T[] ByType(bool includeInactive = false, bool orderByIdentifier #endif return results; } + + /// + /// Returns an enumerator that enumerates over all the components of a given type in a scene. + /// + /// The scene to use for searching + /// When true, inactive objects will be included. + /// Type of to get from the scene + /// a generator that yields successive NetworkObjects in the current scene + public static IEnumerable FromSceneByType(Scene scene, bool includeInactive) where T : UnityEngine.Component + { + return new ObjectsInSceneEnumerator(scene, includeInactive); + } + + /// + /// An Enumerator that enumerates over each component of type in the given scene. + /// + /// Type of to get from the scene + private struct ObjectsInSceneEnumerator : IEnumerable, IEnumerator where T : UnityEngine.Component + { + private readonly UnityEngine.GameObject[] m_RootObjects; + private int m_RootIndex; + private T[] m_CurrentChildObjects; + private int m_CurrentChildIndex; + + private readonly bool m_IncludeInactive; + + internal ObjectsInSceneEnumerator(Scene scene, bool includeInactive) + { + m_IncludeInactive = includeInactive; + + m_RootObjects = scene.GetRootGameObjects(); + m_RootIndex = 0; + m_CurrentChildObjects = null; + m_CurrentChildIndex = 0; + Current = null; + } + + public void Dispose() { } + + public bool MoveNext() + { + while (m_CurrentChildObjects == null && m_RootIndex < m_RootObjects.Length) + { + m_CurrentChildObjects = m_RootObjects[m_RootIndex].GetComponentsInChildren(m_IncludeInactive); + m_RootIndex++; + + if (m_CurrentChildObjects.Length == 0) + { + m_CurrentChildObjects = null; + } + } + + if (m_CurrentChildObjects != null && m_CurrentChildIndex < m_CurrentChildObjects.Length) + { + Current = m_CurrentChildObjects[m_CurrentChildIndex]; + m_CurrentChildIndex++; + + if (m_CurrentChildIndex >= m_CurrentChildObjects.Length) + { + m_CurrentChildIndex = 0; + m_CurrentChildObjects = null; + } + return true; + } + + Current = null; + return false; + } + + public void Reset() + { + m_RootIndex = 0; + m_CurrentChildObjects = null; + m_CurrentChildIndex = 0; + Current = null; + } + + object IEnumerator.Current => Current; + + public T Current { get; private set; } + + public IEnumerator GetEnumerator() => this; + + IEnumerator IEnumerable.GetEnumerator() => this; + } } } diff --git a/com.unity.netcode.gameobjects/Runtime/Core/NetworkManager.cs b/com.unity.netcode.gameobjects/Runtime/Core/NetworkManager.cs index 55fdd6dbf8..2e7e3a7572 100644 --- a/com.unity.netcode.gameobjects/Runtime/Core/NetworkManager.cs +++ b/com.unity.netcode.gameobjects/Runtime/Core/NetworkManager.cs @@ -1644,7 +1644,7 @@ internal void ShutdownInternal() // place (i.e. sending any last state updates or the like). SpawnManager?.DespawnAndDestroyNetworkObjects(); - SpawnManager?.ServerResetShudownStateForSceneObjects(); + SpawnManager?.ServerResetShutdownStateForSceneObjects(); //// RpcTarget?.Dispose(); diff --git a/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs b/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs index d2f5aa5869..173d7fba8d 100644 --- a/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs +++ b/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs @@ -291,7 +291,7 @@ internal void OnValidate() if (GlobalObjectIdHash != oldValue) { // Check if this is an in-scnee placed NetworkObject (Special Case for In-Scene Placed). - if (IsSceneObject.HasValue && IsSceneObject.Value) + if (InScenePlaced) { // Sanity check to make sure this is a scene placed object. if (globalId.identifierType != k_SceneObjectType) @@ -340,7 +340,12 @@ private void CheckForInScenePlaced() EditorUtility.SetDirty(this); } } - IsSceneObject = true; + +#pragma warning disable CS0618 // Type or member is obsolete + // Obsolete with warning means we need the underlying behaviour to keep existing + // TODO-3.x: remove in the 3.x branch + SetSceneObjectStatus(true); +#pragma warning restore CS0618 // Type or member is obsolete // Default scene migration synchronization to false for in-scene placed NetworkObjects SceneMigrationSynchronization = false; @@ -1225,15 +1230,24 @@ private bool InternalHasAuthority() public bool IsSpawned { get; internal set; } /// - /// Gets if the object is a SceneObject, null if it's not yet spawned but is a scene object. + /// Gets if the object is a SceneObject. /// + [Obsolete("Use InScenePlaced instead")] public bool? IsSceneObject { get; internal set; } - //DANGOEXP TODO: Determine if we want to keep this + /// + /// True if this object is placed in a scene; false otherwise. + /// + [field: HideInInspector] + [field: SerializeField] + public bool InScenePlaced { get; internal set; } + /// /// Sets whether this NetworkObject was instantiated as part of a scene /// + /// Only use this when using custom scene loading /// When true, marks this as a scene-instantiated object; when false, marks it as runtime-instantiated + [Obsolete("SetSceneObjectStatus is now calculated during the build.")] public void SetSceneObjectStatus(bool isSceneObject = false) { IsSceneObject = isSceneObject; @@ -1457,7 +1471,7 @@ internal Scene SceneOrigin /// internal NetworkSceneHandle GetSceneOriginHandle() { - if (SceneOriginHandle.IsEmpty() && IsSpawned && IsSceneObject != false) + if (SceneOriginHandle.IsEmpty() && IsSpawned && InScenePlaced) { if (NetworkManager.LogLevel <= LogLevel.Error) { @@ -1622,7 +1636,7 @@ public void NetworkHide(ulong clientId) var message = new DestroyObjectMessage { NetworkObjectId = NetworkObjectId, - DestroyGameObject = !IsSceneObject.Value, + DestroyGameObject = !InScenePlaced, IsDistributedAuthority = NetworkManagerOwner.DistributedAuthorityMode, IsTargetedDestroy = NetworkManagerOwner.DistributedAuthorityMode, TargetClientId = clientId, // Just always populate this value whether we write it or not @@ -1750,7 +1764,7 @@ private void OnDestroy() var isStillValid = gameObject != null && gameObject.scene.IsValid() && gameObject.scene.isLoaded; // If we're not the authority and everything is valid and dynamically spawned, then the destroy is not valid. - if (!isAuthorityDestroy && IsSceneObject == false && isStillValid) + if (!isAuthorityDestroy && !InScenePlaced && isStillValid) { if (networkManager.LogLevel <= LogLevel.Error) { @@ -1849,7 +1863,7 @@ internal void SpawnInternal(bool destroyWithScene, ulong ownerClientId, bool pla } } - if (!NetworkManagerOwner.SpawnManager.AuthorityLocalSpawn(this, NetworkManagerOwner.SpawnManager.GetNetworkObjectId(), IsSceneObject.HasValue && IsSceneObject.Value, playerObject, ownerClientId, destroyWithScene)) + if (!NetworkManagerOwner.SpawnManager.AuthorityLocalSpawn(this, NetworkManagerOwner.SpawnManager.GetNetworkObjectId(), InScenePlaced, playerObject, ownerClientId, destroyWithScene)) { if (NetworkManagerOwner.LogLevel <= LogLevel.Normal) { @@ -2528,8 +2542,7 @@ internal bool ApplyNetworkParenting(bool removeParent = false, bool ignoreNotSpa // Handle the first in-scene placed NetworkObject parenting scenarios. Once the m_LatestParent // has been set, this will not be entered into again (i.e. the later code will be invoked and // users will get notifications when the parent changes). - var isInScenePlaced = IsSceneObject.HasValue && IsSceneObject.Value; - if (transform.parent != null && !removeParent && !m_LatestParent.HasValue && isInScenePlaced) + if (transform.parent != null && !removeParent && !m_LatestParent.HasValue && InScenePlaced) { var parentNetworkObject = transform.parent.GetComponent(); @@ -3271,7 +3284,7 @@ internal SerializedObject Serialize(ulong targetClientId = NetworkManager.Server NetworkObjectId = NetworkObjectId, OwnerClientId = OwnerClientId, IsPlayerObject = IsPlayerObject, - IsSceneObject = IsSceneObject ?? true, + IsSceneObject = InScenePlaced, DestroyWithScene = DestroyWithScene, DontDestroyWithOwner = DontDestroyWithOwner, HasOwnershipFlags = NetworkManagerOwner.DistributedAuthorityMode, @@ -3456,7 +3469,7 @@ internal void SubscribeToActiveSceneForSynch() { if (ActiveSceneSynchronization) { - if (IsSceneObject.HasValue && !IsSceneObject.Value) + if (!InScenePlaced) { // Just in case it is a recycled NetworkObject, unsubscribe first SceneManager.activeSceneChanged -= CurrentlyActiveSceneChanged; @@ -3473,7 +3486,7 @@ private void CurrentlyActiveSceneChanged(Scene current, Scene next) { // Early exit if the NetworkObject is not spawned, is an in-scene placed NetworkObject, // or the NetworkManager is shutting down. - if (!IsSpawned || IsSceneObject != false || NetworkManagerOwner.ShutdownInProgress) + if (!IsSpawned || NetworkManagerOwner.ShutdownInProgress || InScenePlaced) { return; } @@ -3483,7 +3496,7 @@ private void CurrentlyActiveSceneChanged(Scene current, Scene next) { // Only dynamically spawned NetworkObjects that are not already in the newly assigned active scene will migrate // and update their scene handles - if (IsSceneObject.HasValue && !IsSceneObject.Value && gameObject.scene != next && gameObject.transform.parent == null) + if (gameObject.scene != next && gameObject.transform.parent == null) { SceneManager.MoveGameObjectToScene(gameObject, next); SceneChangedUpdate(next); @@ -3571,7 +3584,7 @@ internal bool UpdateForSceneChanges() // the NetworkManager is shutting down, the NetworkObject is not spawned, it is an in-scene placed // NetworkObject, or the GameObject's current scene handle is the same as the SceneOriginHandle if (!SceneMigrationSynchronization || !IsSpawned || NetworkManagerOwner.ShutdownInProgress || - !NetworkManagerOwner.NetworkConfig.EnableSceneManagement || IsSceneObject != false || !gameObject) + !NetworkManagerOwner.NetworkConfig.EnableSceneManagement || InScenePlaced || !gameObject) { // Stop checking for a scene migration return false; @@ -3606,7 +3619,7 @@ internal uint CheckForGlobalObjectIdHashOverride() // If scene management is disabled and this is an in-scene placed NetworkObject then go ahead // and send the InScenePlacedSourcePrefab's GlobalObjectIdHash value (i.e. what to dynamically spawn) - if (!networkManager.NetworkConfig.EnableSceneManagement && IsSceneObject.Value && InScenePlacedSourceGlobalObjectIdHash != 0) + if (!networkManager.NetworkConfig.EnableSceneManagement && InScenePlaced && InScenePlacedSourceGlobalObjectIdHash != 0) { return InScenePlacedSourceGlobalObjectIdHash; } @@ -3614,7 +3627,7 @@ internal uint CheckForGlobalObjectIdHashOverride() // If the PrefabGlobalObjectIdHash is a non-zero value and the GlobalObjectIdHash value is // different from the PrefabGlobalObjectIdHash value, then the NetworkObject instance is // an override for the original network prefab (i.e. PrefabGlobalObjectIdHash) - if (!IsSceneObject.Value && GlobalObjectIdHash != PrefabGlobalObjectIdHash) + if (!InScenePlaced && GlobalObjectIdHash != PrefabGlobalObjectIdHash) { // If the PrefabGlobalObjectIdHash is already populated (i.e. InstantiateAndSpawn used), then return this if (PrefabGlobalObjectIdHash != 0) diff --git a/com.unity.netcode.gameobjects/Runtime/SceneManagement/DefaultSceneManagerHandler.cs b/com.unity.netcode.gameobjects/Runtime/SceneManagement/DefaultSceneManagerHandler.cs index 5a11a01b4a..6e3954b294 100644 --- a/com.unity.netcode.gameobjects/Runtime/SceneManagement/DefaultSceneManagerHandler.cs +++ b/com.unity.netcode.gameobjects/Runtime/SceneManagement/DefaultSceneManagerHandler.cs @@ -311,7 +311,7 @@ public void MoveObjectsFromSceneToDontDestroyOnLoad(ref NetworkManager networkMa if (!networkObject.DestroyWithScene && networkObject.gameObject.scene != networkManager.SceneManager.DontDestroyOnLoadScene) { // Only move dynamically spawned NetworkObjects with no parent as the children will follow - if (networkObject.gameObject.transform.parent == null && networkObject.IsSceneObject != null && !networkObject.IsSceneObject.Value) + if (networkObject.gameObject.transform.parent == null && !networkObject.InScenePlaced) { UnityEngine.Object.DontDestroyOnLoad(networkObject.gameObject); } diff --git a/com.unity.netcode.gameobjects/Runtime/SceneManagement/NetworkSceneHandle.cs b/com.unity.netcode.gameobjects/Runtime/SceneManagement/NetworkSceneHandle.cs index e544eb1f24..f57414bcc5 100644 --- a/com.unity.netcode.gameobjects/Runtime/SceneManagement/NetworkSceneHandle.cs +++ b/com.unity.netcode.gameobjects/Runtime/SceneManagement/NetworkSceneHandle.cs @@ -34,7 +34,6 @@ public void NetworkSerialize(BufferSerializer serializer) where T : IReade else { var reader = serializer.GetFastBufferReader(); - // DANGO-TODO Rust needs to be updated to either handle this ulong or to remove the scene store. #if SCENE_MANAGEMENT_SCENE_HANDLE_MUST_USE_ULONG reader.ReadValueSafe(out ulong rawData); m_Handle = SceneHandle.FromRawData(rawData); @@ -87,6 +86,11 @@ internal NetworkSceneHandle(int handle, bool asMock) public int GetRawData() => m_Handle; #endif + public override string ToString() + { + return m_Handle.ToString(); + } + #region Implicit conversions #if SCENE_MANAGEMENT_SCENE_HANDLE_AVAILABLE /// diff --git a/com.unity.netcode.gameobjects/Runtime/SceneManagement/NetworkSceneManager.cs b/com.unity.netcode.gameobjects/Runtime/SceneManagement/NetworkSceneManager.cs index 2fbdf1fbe9..78bdc7204a 100644 --- a/com.unity.netcode.gameobjects/Runtime/SceneManagement/NetworkSceneManager.cs +++ b/com.unity.netcode.gameobjects/Runtime/SceneManagement/NetworkSceneManager.cs @@ -2234,7 +2234,7 @@ private void SynchronizeNetworkObjectScene() // This is only done for dynamically spawned NetworkObjects // Theoretically, a server could have NetworkObjects in a server-side only scene, if the client doesn't have that scene loaded // then skip it (it will reside in the currently active scene in this scenario on the client-side) - if (networkObject.IsSceneObject.Value == false && ServerSceneHandleToClientSceneHandle.ContainsKey(networkObject.NetworkSceneHandle)) + if (!networkObject.InScenePlaced && ServerSceneHandleToClientSceneHandle.ContainsKey(networkObject.NetworkSceneHandle)) { networkObject.SceneOriginHandle = ServerSceneHandleToClientSceneHandle[networkObject.NetworkSceneHandle]; @@ -2709,7 +2709,7 @@ internal void MoveObjectsToDontDestroyOnLoad() if (!networkObject.DestroyWithScene) { // Only move dynamically spawned NetworkObjects with no parent as the children will follow - if (networkObject.gameObject.transform.parent == null && networkObject.IsSceneObject != null && !networkObject.IsSceneObject.Value) + if (networkObject.gameObject.transform.parent == null && !networkObject.InScenePlaced) { UnityEngine.Object.DontDestroyOnLoad(networkObject.gameObject); // When temporarily migrating to the DDOL, adjust the network and origin scene handles so no messages are generated @@ -2721,10 +2721,9 @@ internal void MoveObjectsToDontDestroyOnLoad() else if (networkObject.HasAuthority) { networkObject.SetIsDestroying(); - var isSceneObject = networkObject.IsSceneObject; // Only destroy non-scene placed NetworkObjects to avoid warnings about destroying in-scene placed NetworkObjects. // (MoveObjectsToDontDestroyOnLoad is only invoked during a scene event type of load and the load scene mode is single) - networkObject.Despawn(isSceneObject.HasValue && isSceneObject.Value == false); + networkObject.Despawn(!networkObject.InScenePlaced); } } } @@ -2745,19 +2744,27 @@ internal void PopulateScenePlacedObjects(Scene sceneToFilterBy, bool clearSceneP { ScenePlacedObjects.Clear(); } - var networkObjects = FindObjects.ByType(); + var sceneHandle = sceneToFilterBy.handle; // Just add every NetworkObject found that isn't already in the list // With additive scenes, we can have multiple in-scene placed NetworkObjects with the same GlobalObjectIdHash value // During Client Side Synchronization: We add them on a FIFO basis, for each scene loaded without clearing, and then // at the end of scene loading we use this list to soft synchronize all in-scene placed NetworkObjects - foreach (var networkObjectInstance in networkObjects) + foreach (var networkObjectInstance in FindObjects.FromSceneByType(sceneToFilterBy, true)) { + if (!networkObjectInstance.InScenePlaced) + { + continue; + } + + if (networkObjectInstance.NetworkManagerOwner == null) + { + networkObjectInstance.NetworkManagerOwner = NetworkManager; + } + var globalObjectIdHash = networkObjectInstance.GlobalObjectIdHash; - var sceneHandle = networkObjectInstance.gameObject.scene.handle; - // We check to make sure the NetworkManager instance is the same one to be "NetcodeIntegrationTestHelpers" compatible and filter the list on a per scene basis (for additive scenes) - if (networkObjectInstance.IsSceneObject != false && (networkObjectInstance.NetworkManager == NetworkManager || - networkObjectInstance.NetworkManagerOwner == null) && sceneHandle == sceneToFilterBy.handle) + // We check to make sure the NetworkManager instance is the same one to be "NetcodeIntegrationTestHelpers" compatible and filter the list on a per-scene basis (for additive scenes) + if (networkObjectInstance.NetworkManagerOwner == NetworkManager && networkObjectInstance.isActiveAndEnabled) { if (!ScenePlacedObjects.ContainsKey(globalObjectIdHash)) { @@ -2795,7 +2802,7 @@ internal void MoveObjectsFromDontDestroyOnLoadToScene(Scene scene) { // only move dynamically spawned network objects, with no parent as child objects will follow, // back into the currently active scene - if (networkObject.gameObject.transform.parent == null && networkObject.IsSceneObject != null && !networkObject.IsSceneObject.Value) + if (networkObject.gameObject.transform.parent == null && !networkObject.InScenePlaced) { if (NetworkManager.DistributedAuthorityMode) { @@ -2879,7 +2886,7 @@ internal void NotifyNetworkObjectSceneChanged(NetworkObject networkObject) } // Ignore in-scene placed NetworkObjects - if (networkObject.IsSceneObject != false) + if (networkObject.InScenePlaced) { // Really, this should ever happen but in case it does if (NetworkManager.LogLevel == LogLevel.Developer) diff --git a/com.unity.netcode.gameobjects/Runtime/SceneManagement/SceneEventData.cs b/com.unity.netcode.gameobjects/Runtime/SceneManagement/SceneEventData.cs index 0fff313574..0f41aad482 100644 --- a/com.unity.netcode.gameobjects/Runtime/SceneManagement/SceneEventData.cs +++ b/com.unity.netcode.gameobjects/Runtime/SceneManagement/SceneEventData.cs @@ -133,7 +133,6 @@ internal class SceneEventData : IDisposable private List m_NetworkObjectsSync = new List(); private List m_DespawnedInSceneObjectsSync = new List(); - private Dictionary> m_DespawnedInSceneObjects = new Dictionary>(); /// /// Server Side Re-Synchronization: @@ -369,13 +368,19 @@ internal void AddDespawnedInSceneNetworkObjects() { m_DespawnedInSceneObjectsSync.Clear(); // Find all active and non-active in-scene placed NetworkObjects - var inSceneNetworkObjects = FindObjects.ByType(true, true).Where((c) => c.NetworkManager == m_NetworkManager); - foreach (var sobj in inSceneNetworkObjects) + foreach (var scene in m_NetworkManager.SceneManager.ScenesLoaded.Values) { - if (sobj.IsSceneObject.HasValue && sobj.IsSceneObject.Value && !sobj.IsSpawned) + // Ignore invalid scenes + if (!scene.IsValid()) { - sobj.NetworkManagerOwner = m_NetworkManager; - m_DespawnedInSceneObjectsSync.Add(sobj); + continue; + } + foreach (var networkObject in FindObjects.FromSceneByType(scene, true)) + { + if (networkObject.InScenePlaced && networkObject.NetworkManagerOwner == m_NetworkManager && !networkObject.IsSpawned) + { + m_DespawnedInSceneObjectsSync.Add(networkObject); + } } } } @@ -1009,7 +1014,6 @@ internal void WriteClientSynchronizationResults(FastBufferWriter writer) private void DeserializeDespawnedInScenePlacedNetworkObjects() { // Process all de-spawned in-scene NetworkObjects for this network session - m_DespawnedInSceneObjects.Clear(); InternalBuffer.ReadValueSafe(out int despawnedObjectsCount); var sceneCache = new Dictionary>(); @@ -1018,25 +1022,21 @@ private void DeserializeDespawnedInScenePlacedNetworkObjects() // We just need to get the scene InternalBuffer.ReadValueSafe(out NetworkSceneHandle networkSceneHandle); InternalBuffer.ReadValueSafe(out uint globalObjectIdHash); - var sceneRelativeNetworkObjects = new Dictionary(); - if (!sceneCache.ContainsKey(networkSceneHandle)) + + // Check if we already have processed the objects in this scene + if (!sceneCache.TryGetValue(networkSceneHandle, out var sceneRelativeNetworkObjects)) { - if (m_NetworkManager.SceneManager.ServerSceneHandleToClientSceneHandle.ContainsKey(networkSceneHandle)) + // If we haven't already cached the objects in this scene, build the cache + sceneRelativeNetworkObjects = new Dictionary(); + if (m_NetworkManager.SceneManager.ServerSceneHandleToClientSceneHandle.TryGetValue(networkSceneHandle, out var localSceneHandle)) { - var localSceneHandle = m_NetworkManager.SceneManager.ServerSceneHandleToClientSceneHandle[networkSceneHandle]; - if (m_NetworkManager.SceneManager.ScenesLoaded.ContainsKey(localSceneHandle)) + if (m_NetworkManager.SceneManager.ScenesLoaded.TryGetValue(localSceneHandle, out var objectRelativeScene)) { - var objectRelativeScene = m_NetworkManager.SceneManager.ScenesLoaded[localSceneHandle]; - - // Find all active and non-active in-scene placed NetworkObjects - var inSceneNetworkObjects = FindObjects.ByType(true, true).Where((c) => - c.GetSceneOriginHandle() == localSceneHandle && (c.IsSceneObject != false)).ToList(); - - foreach (var inSceneObject in inSceneNetworkObjects) + foreach (var networkObject in FindObjects.FromSceneByType(objectRelativeScene, true)) { - if (!sceneRelativeNetworkObjects.ContainsKey(inSceneObject.GlobalObjectIdHash)) + if (networkObject.InScenePlaced) { - sceneRelativeNetworkObjects.Add(inSceneObject.GlobalObjectIdHash, inSceneObject); + sceneRelativeNetworkObjects.TryAdd(networkObject.GlobalObjectIdHash, networkObject); } } // Add this to a cache so we don't have to run this potentially multiple times (nothing will spawn or despawn during this time @@ -1052,10 +1052,6 @@ private void DeserializeDespawnedInScenePlacedNetworkObjects() UnityEngine.Debug.LogError($"In-Scene NetworkObject GlobalObjectIdHash ({globalObjectIdHash}) cannot find its relative NetworkSceneHandle {networkSceneHandle}!"); } } - else // Use the cached NetworkObjects if they exist - { - sceneRelativeNetworkObjects = sceneCache[networkSceneHandle]; - } // Now find the in-scene NetworkObject with the current GlobalObjectIdHash we are looking for if (sceneRelativeNetworkObjects.TryGetValue(globalObjectIdHash, out var despawnedObject)) @@ -1143,7 +1139,7 @@ internal void SynchronizeSceneNetworkObjects(NetworkManager networkManager) // Notify that all in-scene placed NetworkObjects have been spawned foreach (var networkObject in m_NetworkObjectsSync) { - if (networkObject.IsSceneObject.HasValue && networkObject.IsSceneObject.Value) + if (networkObject.IsSpawned && networkObject.InScenePlaced) { networkObject.InternalInSceneNetworkObjectsSpawned(); } diff --git a/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkSpawnManager.cs b/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkSpawnManager.cs index 7a2f45b1d1..46f52bc4f9 100644 --- a/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkSpawnManager.cs +++ b/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkSpawnManager.cs @@ -1320,11 +1320,15 @@ internal bool SpawnNetworkObjectLocallyCommon(NetworkObject networkObject, ulong return false; } - networkObject.IsSceneObject = sceneObject; +#pragma warning disable CS0618 // Type or member is obsolete + // Obsolete with warning means we need the underlying behaviour to keep existing + // TODO: remove in the 3.x branch + networkObject.SetSceneObjectStatus(sceneObject); +#pragma warning restore CS0618 // Type or member is obsolete // Always check to make sure our scene of origin is properly set for in-scene placed NetworkObjects // Note: Always check SceneOriginHandle directly at this specific location. - if (networkObject.IsSceneObject != false && networkObject.SceneOriginHandle.IsEmpty()) + if (networkObject.InScenePlaced && networkObject.SceneOriginHandle.IsEmpty()) { networkObject.SceneOrigin = networkObject.gameObject.scene; } @@ -1379,17 +1383,6 @@ internal bool SpawnNetworkObjectLocallyCommon(NetworkObject networkObject, ulong networkObject.InvokeBehaviourNetworkSpawn(); - // propagate the IsSceneObject setting to child NetworkObjects - var children = networkObject.GetComponentsInChildren(); - foreach (var childObject in children) - { - // Do not propagate the in-scene object setting if a child was dynamically spawned. - if (childObject.IsSceneObject.HasValue && !childObject.IsSceneObject.Value) - { - continue; - } - childObject.IsSceneObject = sceneObject; - } // Only dynamically spawned NetworkObjects are allowed if (!sceneObject) @@ -1404,7 +1397,7 @@ internal bool SpawnNetworkObjectLocallyCommon(NetworkObject networkObject, ulong // If we are an in-scene placed NetworkObject and our InScenePlacedSourceGlobalObjectIdHash is set // then assign this to the PrefabGlobalObjectIdHash - if (networkObject.IsSceneObject.Value && networkObject.InScenePlacedSourceGlobalObjectIdHash != 0) + if (networkObject.InScenePlaced && networkObject.InScenePlacedSourceGlobalObjectIdHash != 0) { networkObject.PrefabGlobalObjectIdHash = networkObject.InScenePlacedSourceGlobalObjectIdHash; } @@ -1552,14 +1545,17 @@ internal void DespawnObject(NetworkObject networkObject, bool destroyObject = fa } // Makes scene objects ready to be reused - internal void ServerResetShudownStateForSceneObjects() + internal void ServerResetShutdownStateForSceneObjects() { - var networkObjects = FindObjects.ByType(orderByIdentifier: true).Where((c) => c.IsSceneObject != null && c.IsSceneObject == true); + var networkObjects = FindObjects.ByType(orderByIdentifier: true, includeInactive: true); foreach (var sobj in networkObjects) { + if (!sobj.InScenePlaced) + { + continue; + } sobj.IsSpawned = false; sobj.DestroyWithScene = false; - sobj.IsSceneObject = null; } } @@ -1575,7 +1571,7 @@ internal void ServerDestroySpawnedSceneObjects() foreach (var networkObject in spawnedObjects) { - if (networkObject.IsSceneObject != null && networkObject.IsSceneObject.Value && networkObject.DestroyWithScene + if (networkObject.InScenePlaced && networkObject.DestroyWithScene && networkObject.gameObject.scene != NetworkManager.SceneManager.DontDestroyOnLoadScene) { if (networkObject.IsSpawned && networkObject.HasAuthority) @@ -1619,7 +1615,7 @@ internal void DespawnAndDestroyNetworkObjects() { // 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 = !(networkObject.IsSceneObject == null || (networkObject.IsSceneObject != null && networkObject.IsSceneObject.Value)); + var shouldDestroy = !networkObject.InScenePlaced; // If we are going to destroy this NetworkObject, check for any in-scene placed children that need to be removed if (shouldDestroy) @@ -1635,7 +1631,7 @@ internal void DespawnAndDestroyNetworkObjects() // If the child is an in-scene placed NetworkObject then remove the child from the parent (which was dynamically spawned) // and set its parent to root - if (childObject.IsSceneObject != null && childObject.IsSceneObject.Value) + if (childObject.InScenePlaced) { childObject.TryRemoveParentCachedWorldPositionStays(); } @@ -1654,27 +1650,24 @@ internal void DestroySceneObjects() for (int i = 0; i < networkObjects.Length; i++) { - if (networkObjects[i].NetworkManager == NetworkManager) + if (networkObjects[i].NetworkManager == NetworkManager && networkObjects[i].InScenePlaced) { - if (networkObjects[i].IsSceneObject == null || networkObjects[i].IsSceneObject.Value == true) + if (NetworkManager.PrefabHandler.ContainsHandler(networkObjects[i])) { - if (NetworkManager.PrefabHandler.ContainsHandler(networkObjects[i])) + if (SpawnedObjects.ContainsKey(networkObjects[i].NetworkObjectId)) { - if (SpawnedObjects.ContainsKey(networkObjects[i].NetworkObjectId)) - { - // This method invokes HandleNetworkPrefabDestroy, we only want to handle this once. - OnDespawnObject(networkObjects[i], false); - } - else // If not spawned, then just invoke the handler - { - NetworkManager.PrefabHandler.HandleNetworkPrefabDestroy(networkObjects[i]); - } + // This method invokes HandleNetworkPrefabDestroy, we only want to handle this once. + OnDespawnObject(networkObjects[i], false); } - else + else // If not spawned, then just invoke the handler { - Object.Destroy(networkObjects[i].gameObject); + NetworkManager.PrefabHandler.HandleNetworkPrefabDestroy(networkObjects[i]); } } + else + { + Object.Destroy(networkObjects[i].gameObject); + } } } } @@ -1693,7 +1686,7 @@ internal void ServerSpawnSceneObjectsOnStartSweep() // This used to be two loops. // The first added all NetworkObjects to a list and the second spawned all NetworkObjects in the list. // Now, a parent will set its children's IsSceneObject value when spawned, so we check for null or for true. - if (networkObject.IsSceneObject == null || (networkObject.IsSceneObject.HasValue && networkObject.IsSceneObject.Value)) + if (networkObject.InScenePlaced) { var ownerId = networkObject.OwnerClientId; if (NetworkManager.DistributedAuthorityMode) @@ -1744,7 +1737,7 @@ internal void OnDespawnNonAuthorityObject([NotNull] NetworkObject networkObject, } } - if (networkObject.IsSceneObject == false) + if (!networkObject.InScenePlaced) { // If the object is not an in-scene placed NetworkObject, then we always destroy the object on the non-authority side destroyGameObject = true; @@ -1786,7 +1779,7 @@ internal void OnDespawnObject([NotNull] NetworkObject networkObject, bool destro // DistributedAuthorityMode: All clients need to remove the parent locally due to mixed-authority hierarchies and race-conditions if (!NetworkManager.ShutdownInProgress && (NetworkManager.IsServer || distributedAuthority)) { - if (destroyGameObject && networkObject.IsSceneObject == true && !NetworkManager.SceneManager.IsSceneUnloading(networkObject)) + if (destroyGameObject && networkObject.InScenePlaced && !NetworkManager.SceneManager.IsSceneUnloading(networkObject)) { if (NetworkManager.LogLevel <= LogLevel.Normal) { @@ -2113,7 +2106,7 @@ internal void GetObjectDistribution(ulong clientId, ref Dictionary(); networkObject.IsSpawned = true; networkObject.SceneOriginHandle = default; - networkObject.IsSceneObject = false; // This validates invoking GetSceneOriginHandle will not throw an exception for a dynamically spawned NetworkObject // when the scene of origin hasn't been set. var sceneOriginHandle = networkObject.GetSceneOriginHandle(); diff --git a/com.unity.netcode.gameobjects/Tests/Runtime/NetworkObject/NetworkObjectSpawnManyObjectsTests.cs b/com.unity.netcode.gameobjects/Tests/Runtime/NetworkObject/NetworkObjectSpawnManyObjectsTests.cs index cf786bd0c4..ad643d7ca7 100644 --- a/com.unity.netcode.gameobjects/Tests/Runtime/NetworkObject/NetworkObjectSpawnManyObjectsTests.cs +++ b/com.unity.netcode.gameobjects/Tests/Runtime/NetworkObject/NetworkObjectSpawnManyObjectsTests.cs @@ -37,7 +37,6 @@ protected override void OnServerAndClientsCreated() var gameObject = new GameObject("TestObject"); var networkObject = gameObject.AddComponent(); NetcodeIntegrationTestHelpers.MakeNetworkObjectTestPrefab(networkObject); - networkObject.IsSceneObject = false; gameObject.AddComponent(); m_PrefabToSpawn = new NetworkPrefab() { Prefab = gameObject }; diff --git a/com.unity.netcode.gameobjects/Tests/Runtime/NetworkVariable/NetworkVariableBaseInitializesWhenPersisted.cs b/com.unity.netcode.gameobjects/Tests/Runtime/NetworkVariable/NetworkVariableBaseInitializesWhenPersisted.cs index 0245602fcd..6e8dc8cde9 100644 --- a/com.unity.netcode.gameobjects/Tests/Runtime/NetworkVariable/NetworkVariableBaseInitializesWhenPersisted.cs +++ b/com.unity.netcode.gameobjects/Tests/Runtime/NetworkVariable/NetworkVariableBaseInitializesWhenPersisted.cs @@ -26,7 +26,6 @@ protected override void OnOneTimeSetup() s_NetworkPrefab = new GameObject("PresistPrefab"); var networkObject = s_NetworkPrefab.AddComponent(); networkObject.GlobalObjectIdHash = 8888888; - networkObject.SetSceneObjectStatus(false); s_NetworkPrefab.AddComponent(); s_NetworkPrefab.AddComponent(); // Create enough prefab instance handlers to be re-used for all tests. @@ -374,7 +373,6 @@ public NetworkObject GetInstance() if (PrefabInstances.Count == 0) { instanceToReturn = Object.Instantiate(m_NetworkPrefab).GetComponent(); - instanceToReturn.SetSceneObjectStatus(false); instanceToReturn.gameObject.SetActive(true); } else diff --git a/com.unity.netcode.gameobjects/Tests/Runtime/Prefabs/NetworkPrefabOverrideTests.cs b/com.unity.netcode.gameobjects/Tests/Runtime/Prefabs/NetworkPrefabOverrideTests.cs index 502146fa24..571ab30133 100644 --- a/com.unity.netcode.gameobjects/Tests/Runtime/Prefabs/NetworkPrefabOverrideTests.cs +++ b/com.unity.netcode.gameobjects/Tests/Runtime/Prefabs/NetworkPrefabOverrideTests.cs @@ -174,11 +174,6 @@ protected override void OnServerAndClientsCreated() networkManager.NetworkConfig.Prefabs.Add(m_ClientSidePlayerPrefab); } - m_PrefabOverride.Prefab.GetComponent().IsSceneObject = false; - m_PrefabOverride.SourcePrefabToOverride.GetComponent().IsSceneObject = false; - m_PrefabOverride.OverridingTargetPrefab.GetComponent().IsSceneObject = false; - m_ClientSidePlayerPrefab.Prefab.GetComponent().IsSceneObject = false; - base.OnServerAndClientsCreated(); } diff --git a/com.unity.netcode.gameobjects/Tests/Runtime/TestHelpers/IntegrationTestSceneHandler.cs b/com.unity.netcode.gameobjects/Tests/Runtime/TestHelpers/IntegrationTestSceneHandler.cs index 2a5c2c35f9..209caf300b 100644 --- a/com.unity.netcode.gameobjects/Tests/Runtime/TestHelpers/IntegrationTestSceneHandler.cs +++ b/com.unity.netcode.gameobjects/Tests/Runtime/TestHelpers/IntegrationTestSceneHandler.cs @@ -1,7 +1,6 @@ using System; using System.Collections; using System.Collections.Generic; -using System.Linq; using UnityEngine; using UnityEngine.SceneManagement; using Object = UnityEngine.Object; @@ -141,47 +140,27 @@ private static void SceneManager_sceneLoaded(Scene scene, LoadSceneMode loadScen { SceneManager.sceneLoaded -= SceneManager_sceneLoaded; - ProcessInSceneObjects(scene, CurrentQueuedSceneJob.IntegrationTestSceneHandler.NetworkManager); + ProcessInSceneObjects(scene); CurrentQueuedSceneJob.JobType = QueuedSceneJob.JobTypes.Completed; } } /// - /// Handles some pre-spawn processing of in-scene placed NetworkObjects - /// to make sure the appropriate NetworkManagerOwner is assigned. It - /// also makes sure that each in-scene placed NetworkObject has an + /// Handles some pre-spawn processing of in-scene placed NetworkObjects. + /// Makes sure that each in-scene placed NetworkObject has an /// ObjectIdentifier component if one is not assigned to it or its /// children. /// /// the scenes that was just loaded - /// the relative NetworkManager - private static void ProcessInSceneObjects(Scene scene, NetworkManager networkManager) + private static void ProcessInSceneObjects(Scene scene) { - // Get all in-scene placed NeworkObjects that were instantiated when this scene loaded - var inSceneNetworkObjects = FindObjects.ByType(orderByIdentifier: true).Where((c) => c.IsSceneObject != false && c.GetSceneOriginHandle() == scene.handle); - foreach (var sobj in inSceneNetworkObjects) + // Get all in-scene placed NetworkObjects that were instantiated when this scene loaded + foreach (var sceneObject in FindObjects.FromSceneByType(scene, false)) { - ProcessInSceneObject(sobj, networkManager); - } - } - - /// - /// Assures to apply an ObjectNameIdentifier to all children - /// - private static void ProcessInSceneObject(NetworkObject networkObject, NetworkManager networkManager) - { - if (networkObject.GetComponent() == null) - { - networkObject.gameObject.AddComponent(); - var networkObjects = networkObject.gameObject.GetComponentsInChildren(); - foreach (var child in networkObjects) + if (sceneObject.InScenePlaced && sceneObject.GetComponent() == null) { - if (child == networkObject) - { - continue; - } - ProcessInSceneObject(child, networkManager); + sceneObject.gameObject.AddComponent(); } } } @@ -288,7 +267,7 @@ private void Sever_SceneLoaded(Scene scene, LoadSceneMode arg1) if (m_ServerSceneBeingLoaded == scene.name) { SceneManager.sceneLoaded -= Sever_SceneLoaded; - ProcessInSceneObjects(scene, NetworkManager); + ProcessInSceneObjects(scene); } } @@ -708,16 +687,11 @@ public void MoveObjectsFromSceneToDontDestroyOnLoad(ref NetworkManager networkMa { // Create a local copy of the spawned objects list since the spawn manager will adjust the list as objects // are despawned. - var networkObjects = FindObjects.ByType(orderByIdentifier: true).Where((c) => c.IsSpawned); var distributedAuthority = networkManager.DistributedAuthorityMode; - foreach (var networkObject in networkObjects) + foreach (var networkObject in FindObjects.FromSceneByType(scene, false)) { - if (networkObject == null || (networkObject != null && networkObject.gameObject.scene.handle != scene.handle)) + if (!networkObject.IsSpawned) { - if (networkObject != null) - { - VerboseDebug($"[MoveObjects from {scene.name} | {scene.handle}] Ignoring {networkObject.gameObject.name} because it isn't in scene {networkObject.gameObject.scene.name} "); - } continue; } @@ -750,7 +724,7 @@ public void MoveObjectsFromSceneToDontDestroyOnLoad(ref NetworkManager networkMa if (!networkObject.DestroyWithScene && networkObject.gameObject.scene != networkManager.SceneManager.DontDestroyOnLoadScene) { // Only move dynamically spawned NetworkObjects with no parent as the children will follow - if (networkObject.gameObject.transform.parent == null && networkObject.IsSceneObject != null && !networkObject.IsSceneObject.Value) + if (networkObject.gameObject.transform.parent == null && !networkObject.InScenePlaced) { VerboseDebug($"[MoveObjects from {scene.name} | {scene.handle}] Moving {networkObject.gameObject.name} because it is in scene {networkObject.gameObject.scene.name} with DWS = {networkObject.DestroyWithScene}."); Object.DontDestroyOnLoad(networkObject.gameObject); diff --git a/com.unity.netcode.gameobjects/Tests/Runtime/TestHelpers/NetcodeIntegrationTest.cs b/com.unity.netcode.gameobjects/Tests/Runtime/TestHelpers/NetcodeIntegrationTest.cs index a6f4a83275..b9c5623759 100644 --- a/com.unity.netcode.gameobjects/Tests/Runtime/TestHelpers/NetcodeIntegrationTest.cs +++ b/com.unity.netcode.gameobjects/Tests/Runtime/TestHelpers/NetcodeIntegrationTest.cs @@ -728,7 +728,6 @@ private void CreatePlayerPrefab() m_PlayerPrefab = new GameObject("Player"); OnPlayerPrefabGameObjectCreated(); NetworkObject networkObject = m_PlayerPrefab.AddComponent(); - networkObject.IsSceneObject = false; // Make it a prefab NetcodeIntegrationTestHelpers.MakeNetworkObjectTestPrefab(networkObject); diff --git a/com.unity.netcode.gameobjects/Tests/Runtime/TestHelpers/NetcodeIntegrationTestHelpers.cs b/com.unity.netcode.gameobjects/Tests/Runtime/TestHelpers/NetcodeIntegrationTestHelpers.cs index 32a0f734d7..99c7f47afc 100644 --- a/com.unity.netcode.gameobjects/Tests/Runtime/TestHelpers/NetcodeIntegrationTestHelpers.cs +++ b/com.unity.netcode.gameobjects/Tests/Runtime/TestHelpers/NetcodeIntegrationTestHelpers.cs @@ -690,9 +690,6 @@ public static void MakeNetworkObjectTestPrefab(NetworkObject networkObject, uint networkObject.GlobalObjectIdHash = ++s_AutoIncrementGlobalObjectIdHashCounter; } - // Prevent object from being snapped up as a scene object - networkObject.IsSceneObject = false; - // To avoid issues with integration tests that forget to clean up, // this feature only works with NetcodeIntegrationTest derived classes if (IsNetcodeIntegrationTestRunning) diff --git a/com.unity.netcode.gameobjects/package.json b/com.unity.netcode.gameobjects/package.json index 5400c16d26..d2b5daeecd 100644 --- a/com.unity.netcode.gameobjects/package.json +++ b/com.unity.netcode.gameobjects/package.json @@ -2,7 +2,7 @@ "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": "2.12.1", + "version": "2.13.0", "unity": "6000.0", "dependencies": { "com.unity.nuget.mono-cecil": "1.11.4", diff --git a/testproject/Assets/Tests/Runtime/NetworkManagerTests.cs b/testproject/Assets/Tests/Runtime/NetworkManagerTests.cs index 411d7e6fec..ccf46cb61c 100644 --- a/testproject/Assets/Tests/Runtime/NetworkManagerTests.cs +++ b/testproject/Assets/Tests/Runtime/NetworkManagerTests.cs @@ -155,7 +155,7 @@ public IEnumerator ValidateShutdown([Values] ShutdownChecks shutdownCheck) for (int i = spawnedObjects.Count - 1; i >= 0; i--) { var spawnedObject = spawnedObjects[i]; - if (spawnedObject.IsSceneObject != null && spawnedObject.IsSceneObject.Value) + if (spawnedObject.InScenePlaced) { spawnedObject.Despawn(); } diff --git a/testproject/Assets/Tests/Runtime/NetworkSceneManager/AttachableBehaviourSceneLoadTests.cs b/testproject/Assets/Tests/Runtime/NetworkSceneManager/AttachableBehaviourSceneLoadTests.cs index 58d056d8d0..a3e31983e1 100644 --- a/testproject/Assets/Tests/Runtime/NetworkSceneManager/AttachableBehaviourSceneLoadTests.cs +++ b/testproject/Assets/Tests/Runtime/NetworkSceneManager/AttachableBehaviourSceneLoadTests.cs @@ -318,7 +318,7 @@ public IEnumerator AttachedUponSceneTransition([Values] bool detachOnDespawn) var persists = m_Persists == Persists.AttachableNode ? m_TargetInstance.NetworkObjectId : m_SourceInstance.NetworkObjectId; m_DoesNotPersistNetworkObjectIds.Add(doesNotPersist); persistedObjects.Add(networkManager, persists); - Debug.Log($"[{networkManager.name}] Spawned attachable and attached it."); + VerboseDebug($"[{networkManager.name}] Spawned attachable and attached it."); } // This is the actual validation point where the scene is unloaded and either the attachable or diff --git a/testproject/Assets/Tests/Runtime/NetworkSceneManager/ClientSynchronizationModeTests.cs b/testproject/Assets/Tests/Runtime/NetworkSceneManager/ClientSynchronizationModeTests.cs index f69bec2d12..0da0940b00 100644 --- a/testproject/Assets/Tests/Runtime/NetworkSceneManager/ClientSynchronizationModeTests.cs +++ b/testproject/Assets/Tests/Runtime/NetworkSceneManager/ClientSynchronizationModeTests.cs @@ -50,6 +50,8 @@ public enum ActiveSceneStates private Dictionary> m_ClientScenesLoaded = new Dictionary>(); + private readonly List m_ObjectsInScenes = new(); + public ClientSynchronizationModeTests(ServerPreloadStates serverPreloadStates) { @@ -66,9 +68,9 @@ protected override IEnumerator OnSetup() { m_TempClientPreLoadedScenes.Clear(); m_ServerLoadedScenes.Clear(); + m_ObjectsInScenes.Clear(); if (m_ServerPreloadState == ServerPreloadStates.PreloadOnServer) { - SceneManager.sceneLoaded += SceneManager_sceneLoaded; yield return LoadScenesOnServer(); } yield return base.OnSetup(); @@ -78,6 +80,7 @@ private IEnumerator LoadScenesOnServer() { if (m_ServerPreloadState == ServerPreloadStates.PreloadOnServer) { + SceneManager.sceneLoaded += SceneManager_sceneLoaded; foreach (var sceneToLoad in m_TestScenes) { SceneManager.LoadSceneAsync(sceneToLoad, LoadSceneMode.Additive); @@ -88,12 +91,16 @@ private IEnumerator LoadScenesOnServer() } else { + m_ServerNetworkManager.SceneManager.OnSceneEvent += ServerSide_OnSceneEvent; + foreach (var sceneToLoad in m_TestScenes) { m_ServerNetworkManager.SceneManager.LoadScene(sceneToLoad, LoadSceneMode.Additive); yield return WaitForConditionOrTimeOut(() => SceneLoadedOnServer(sceneToLoad)); AssertOnTimeout($"[{m_ServerPreloadState}] Timed out waiting for scene {sceneToLoad} to be loaded!"); } + m_ServerNetworkManager.SceneManager.OnSceneEvent -= ServerSide_OnSceneEvent; + } } @@ -165,9 +172,21 @@ private bool AllScenesLoadedOnClients() return true; } + private void TrackObjectsInScene(Scene scene) + { + foreach (var networkObject in FindObjects.FromSceneByType(scene, true)) + { + if (networkObject.InScenePlaced) + { + m_ObjectsInScenes.Add(networkObject); + } + } + } + private void SceneManager_sceneLoaded(Scene scene, LoadSceneMode loadSceneMode) { m_ServerLoadedScenes.Add(scene); + TrackObjectsInScene(scene); } @@ -203,7 +222,6 @@ public IEnumerator PreloadedScenesTest([Values] ClientPreloadStates clientPreloa // If we didn't preload the scenes, then load the scenes via NetworkSceneManager if (m_ServerPreloadState == ServerPreloadStates.NoPreloadOnServer) { - m_ServerNetworkManager.SceneManager.OnSceneEvent += ServerSide_OnSceneEvent; yield return LoadScenesOnServer(); } @@ -215,6 +233,18 @@ public IEnumerator PreloadedScenesTest([Values] ClientPreloadStates clientPreloa SceneManager.SetActiveScene(m_ServerLoadedScenes[2]); } + var authority = GetAuthorityNetworkManager(); + + foreach (var networkObject in m_ObjectsInScenes) + { + Assert.IsTrue(networkObject.IsSpawned, $"[Client-{authority.LocalClientId}] Server object {networkObject.name}, {networkObject.GlobalObjectIdHash} is not spawned!"); + Assert.AreEqual(networkObject.NetworkManagerOwner, authority, $"[Client-{authority.LocalClientId}] networkObject doesn't belong to the client"); + } + + var objToDisable = m_ObjectsInScenes[0]; + objToDisable.Despawn(false); + + // Late join some clients for (int i = 0; i < 1; i++) { @@ -223,6 +253,7 @@ public IEnumerator PreloadedScenesTest([Values] ClientPreloadStates clientPreloa if (clientPreloadStates == ClientPreloadStates.PreloadOnClient) { m_TempClientPreLoadedScenes.Clear(); + m_ObjectsInScenes.Clear(); SceneManager.sceneLoaded += PreLoadClient_SceneLoaded; foreach (var sceneToLoad in m_TestScenes) { @@ -232,17 +263,37 @@ public IEnumerator PreloadedScenesTest([Values] ClientPreloadStates clientPreloa SceneManager.sceneLoaded -= PreLoadClient_SceneLoaded; AssertOnTimeout($"[{clientPreloadStates}] Timed out waiting for client-side scenes to be preloaded!"); } - yield return CreateAndStartNewClient(); + + var newClient = CreateNewClient(); + yield return StartClient(newClient); AssertOnTimeout($"[Client Instance {i + 1}] Timed out waiting for client to start and connect!"); yield return WaitForConditionOrTimeOut(AllScenesLoadedOnClients); - AssertOnTimeout($"[Client-{m_ClientNetworkManagers[i].LocalClientId}] Timed out waiting for all scenes to be synchronized for new client!"); + AssertOnTimeout($"[Client-{newClient.LocalClientId}] Timed out waiting for all scenes to be synchronized for new client!"); + + if (clientPreloadStates == ClientPreloadStates.PreloadOnClient) + { + foreach (var networkObject in m_ObjectsInScenes) + { + if (networkObject.GlobalObjectIdHash == objToDisable.GlobalObjectIdHash) + { + Assert.IsFalse(networkObject.IsSpawned); + } + else + { + Assert.IsTrue(networkObject.IsSpawned, $"[Client-{newClient.LocalClientId}] Client side preloaded object {networkObject.name}, {networkObject.GlobalObjectIdHash} is not spawned!"); + } + Assert.AreEqual(networkObject.NetworkManagerOwner, newClient, $"[Client-{newClient.LocalClientId}] networkObject doesn't belong to the client"); + } + } } } + private void PreLoadClient_SceneLoaded(Scene scene, LoadSceneMode loadSceneMode) { m_TempClientPreLoadedScenes.Add(scene); + TrackObjectsInScene(scene); } private void ClientSide_OnSceneEvent(SceneEvent sceneEvent) @@ -273,6 +324,7 @@ private void ServerSide_OnSceneEvent(SceneEvent sceneEvent) case SceneEventType.LoadComplete: { m_ServerLoadedScenes.Add(sceneEvent.Scene); + TrackObjectsInScene(sceneEvent.Scene); break; } } @@ -283,6 +335,7 @@ protected override IEnumerator OnTearDown() SceneManager.sceneLoaded -= SceneManager_sceneLoaded; m_TempClientPreLoadedScenes.Clear(); m_ClientScenesLoaded.Clear(); + m_ObjectsInScenes.Clear(); return base.OnTearDown(); } } diff --git a/testproject/Assets/Tests/Runtime/NetworkSceneManager/ClientSynchronizationValidationTest.cs b/testproject/Assets/Tests/Runtime/NetworkSceneManager/ClientSynchronizationValidationTest.cs index 32f88e9221..ea5dfc47ad 100644 --- a/testproject/Assets/Tests/Runtime/NetworkSceneManager/ClientSynchronizationValidationTest.cs +++ b/testproject/Assets/Tests/Runtime/NetworkSceneManager/ClientSynchronizationValidationTest.cs @@ -110,7 +110,7 @@ public IEnumerator ClientVerifySceneBeforeLoading([Values] bool startClientBefor foreach (var spawnedObjectEntry in m_ServerNetworkManager.SpawnManager.SpawnedObjects) { var networkObject = spawnedObjectEntry.Value; - if (!networkObject.IsSceneObject.Value) + if (!networkObject.InScenePlaced) { continue; } @@ -138,7 +138,7 @@ public IEnumerator ClientVerifySceneBeforeLoading([Values] bool startClientBefor foreach (var spawnedObjectEntry in m_ServerNetworkManager.SpawnManager.SpawnedObjects) { var networkObject = spawnedObjectEntry.Value; - if (!networkObject.IsSceneObject.Value) + if (!networkObject.InScenePlaced) { continue; } diff --git a/testproject/Assets/Tests/Runtime/NetworkSceneManager/NetworkSceneManagerDDOLTests.cs b/testproject/Assets/Tests/Runtime/NetworkSceneManager/NetworkSceneManagerDDOLTests.cs index f38a311ea8..70ca7c24d2 100644 --- a/testproject/Assets/Tests/Runtime/NetworkSceneManager/NetworkSceneManagerDDOLTests.cs +++ b/testproject/Assets/Tests/Runtime/NetworkSceneManager/NetworkSceneManagerDDOLTests.cs @@ -94,8 +94,8 @@ public IEnumerator InSceneNetworkObjectState([Values(DefaultState.IsEnabled, Def [Values(NetworkObjectType.InScenePlaced, NetworkObjectType.DynamicallySpawned)] NetworkObjectType networkObjectType) { var waitForFullNetworkTick = new WaitForSeconds(1.0f / m_ServerNetworkManager.NetworkConfig.TickRate); - var isActive = activeState == DefaultState.IsEnabled ? true : false; - var isInScene = networkObjectType == NetworkObjectType.InScenePlaced ? true : false; + var isActive = activeState == DefaultState.IsEnabled; + var isInScene = networkObjectType == NetworkObjectType.InScenePlaced; var objectInstance = Object.Instantiate(m_DDOL_ObjectToSpawn); var networkObject = objectInstance.GetComponent(); @@ -110,7 +110,7 @@ public IEnumerator InSceneNetworkObjectState([Values(DefaultState.IsEnabled, Def } // Sets whether we are in-scene or dynamically spawned NetworkObject - ddolBehaviour.SetInScene(isInScene); + networkObject.InScenePlaced = isInScene; networkObject.Spawn(); yield return waitForFullNetworkTick; @@ -148,12 +148,6 @@ public override void OnNetworkSpawn() NetworkObject.DestroyWithScene = false; base.OnNetworkSpawn(); } - - public void SetInScene(bool isInScene) - { - var networkObject = GetComponent(); - networkObject.IsSceneObject = isInScene; - } } } diff --git a/testproject/Assets/Tests/Runtime/NetworkSceneManager/NetworkSceneManagerEventDataPoolTest.cs b/testproject/Assets/Tests/Runtime/NetworkSceneManager/NetworkSceneManagerEventDataPoolTest.cs index 7c448830f6..107db75314 100644 --- a/testproject/Assets/Tests/Runtime/NetworkSceneManager/NetworkSceneManagerEventDataPoolTest.cs +++ b/testproject/Assets/Tests/Runtime/NetworkSceneManager/NetworkSceneManagerEventDataPoolTest.cs @@ -381,11 +381,11 @@ private bool CheckNetworkObjectsToSynchronizeSceneChanges(NetworkManager network m_ErrorMsg.Clear(); if (networkManager.SpawnManager.NetworkObjectsToSynchronizeSceneChanges.Count > 0) { - foreach (var entry in networkManager.SpawnManager.NetworkObjectsToSynchronizeSceneChanges) + foreach (var obj in networkManager.SpawnManager.NetworkObjectsToSynchronizeSceneChanges.Values) { - if (entry.Value.IsSceneObject.HasValue && entry.Value.IsSceneObject.Value) + if (obj.InScenePlaced) { - m_ErrorMsg.AppendLine($"{entry.Value.name} still exists within {nameof(NetworkSpawnManager.NetworkObjectsToSynchronizeSceneChanges)}!"); + m_ErrorMsg.AppendLine($"{obj.name} still exists within {nameof(NetworkSpawnManager.NetworkObjectsToSynchronizeSceneChanges)}!"); } } } diff --git a/testproject/Assets/Tests/Runtime/NetworkSceneManager/NetworkSceneManagerPopulateInSceneTests.cs b/testproject/Assets/Tests/Runtime/NetworkSceneManager/NetworkSceneManagerPopulateInSceneTests.cs index bc3a24cb78..1d2302592e 100644 --- a/testproject/Assets/Tests/Runtime/NetworkSceneManager/NetworkSceneManagerPopulateInSceneTests.cs +++ b/testproject/Assets/Tests/Runtime/NetworkSceneManager/NetworkSceneManagerPopulateInSceneTests.cs @@ -40,7 +40,7 @@ protected override void OnServerAndClientsCreated() // the scene is loaded (i.e. IsSceneObject is null) var inScenePrefab = CreateNetworkObjectPrefab("NewSceneObject"); var networkObject = inScenePrefab.GetComponent(); - networkObject.IsSceneObject = null; + networkObject.InScenePlaced = true; networkObject.NetworkManagerOwner = m_ServerNetworkManager; m_InSceneObjectList.Add(networkObject.GlobalObjectIdHash, inScenePrefab); @@ -49,7 +49,7 @@ protected override void OnServerAndClientsCreated() // unloading/reloading any scenes. inScenePrefab = CreateNetworkObjectPrefab("SetInSceneObject"); networkObject = inScenePrefab.GetComponent(); - networkObject.IsSceneObject = true; + networkObject.InScenePlaced = true; networkObject.NetworkManagerOwner = m_ServerNetworkManager; m_InSceneObjectList.Add(networkObject.GlobalObjectIdHash, inScenePrefab); } diff --git a/testproject/Assets/Tests/Runtime/ObjectParenting/ParentDynamicUnderInScenePlaced.cs b/testproject/Assets/Tests/Runtime/ObjectParenting/ParentDynamicUnderInScenePlaced.cs index 3bf12e7011..2770ed4ce5 100644 --- a/testproject/Assets/Tests/Runtime/ObjectParenting/ParentDynamicUnderInScenePlaced.cs +++ b/testproject/Assets/Tests/Runtime/ObjectParenting/ParentDynamicUnderInScenePlaced.cs @@ -74,7 +74,7 @@ private bool TestParentedAndNotInScenePlaced() { // Always assign m_FailedValidation to avoid possible null reference crashes. var serverPlayer = m_FailedValidation = m_ServerNetworkManager.LocalClient.PlayerObject; - if (serverPlayer.transform.parent == null || serverPlayer.IsSceneObject.Value == true) + if (serverPlayer.transform.parent == null || serverPlayer.InScenePlaced) { m_FailedValidation = serverPlayer; return false; @@ -83,7 +83,7 @@ private bool TestParentedAndNotInScenePlaced() foreach (var clientNetworkManager in m_ClientNetworkManagers) { var lateJoinPlayer = clientNetworkManager.LocalClient.PlayerObject; - if (lateJoinPlayer.transform.parent == null || lateJoinPlayer.IsSceneObject.Value == true) + if (lateJoinPlayer.transform.parent == null || lateJoinPlayer.InScenePlaced) { m_FailedValidation = lateJoinPlayer; return false; @@ -93,7 +93,7 @@ private bool TestParentedAndNotInScenePlaced() foreach (var dynamicallySpawned in ParentDynamicUnderInScenePlacedHelper.Instances) { var networkObject = dynamicallySpawned.Value; - if (networkObject.transform.parent == null || networkObject.IsSceneObject.Value == true) + if (networkObject.transform.parent == null || networkObject.InScenePlaced) { m_FailedValidation = networkObject; return false; @@ -124,7 +124,7 @@ public IEnumerator ParentUnderInSceneplaced() // Wait for the host-server's player to be parented under the in-scene placed NetworkObject yield return WaitForConditionOrTimeOut(TestParentedAndNotInScenePlaced); - AssertOnTimeout($"[{m_FailedValidation.name}] Failed validation! InScenePlaced ({m_FailedValidation.IsSceneObject.Value}) | Was Parented ({m_FailedValidation.transform.position != null})"); + AssertOnTimeout($"[{m_FailedValidation.name}] Failed validation! InScenePlaced ({m_FailedValidation.InScenePlaced}) | Was Parented ({m_FailedValidation.transform.position != null})"); m_TargetScenePlacedId = m_ServerNetworkManager.LocalClient.PlayerObject.transform.parent.GetComponent().NetworkObjectId; // Now dynamically spawn a NetworkObject to also test dynamically spawned NetworkObjects being parented @@ -141,7 +141,7 @@ public IEnumerator ParentUnderInSceneplaced() AssertOnTimeout($"[Client-{i + 1}] Failed to find in-scene placed NetworkObject or failed to parent under it!"); } yield return WaitForConditionOrTimeOut(TestParentedAndNotInScenePlaced); - AssertOnTimeout($"[{m_FailedValidation.name}] Failed validation! InScenePlaced ({m_FailedValidation.IsSceneObject.Value}) | Was Parented ({m_FailedValidation.transform.position != null})"); + AssertOnTimeout($"[{m_FailedValidation.name}] Failed validation! InScenePlaced ({m_FailedValidation.InScenePlaced}) | Was Parented ({m_FailedValidation.transform.position != null})"); } } diff --git a/testproject/Assets/Tests/Runtime/PrefabExtendedTests.cs b/testproject/Assets/Tests/Runtime/PrefabExtendedTests.cs index d0f7e294cb..909bb7585d 100644 --- a/testproject/Assets/Tests/Runtime/PrefabExtendedTests.cs +++ b/testproject/Assets/Tests/Runtime/PrefabExtendedTests.cs @@ -163,7 +163,7 @@ private bool ValidateAllClientsSpawnedObjects() var clientSpawnedObject = s_GlobalNetworkObjects[client.LocalClientId][spawnedObject.NetworkObjectId]; // When scene management is disabled, we match against the InScenePlacedSourceGlobalObjectIdHash for in-scene placed NetworkObjects - var spawnedObjectGlobalObjectIdHash = !m_SceneManagementEnabled && spawnedObject.IsSceneObject.Value ? spawnedObject.InScenePlacedSourceGlobalObjectIdHash : spawnedObject.GlobalObjectIdHash; + var spawnedObjectGlobalObjectIdHash = !m_SceneManagementEnabled && spawnedObject.InScenePlaced ? spawnedObject.InScenePlacedSourceGlobalObjectIdHash : spawnedObject.GlobalObjectIdHash; // Validate the GlobalObjectIdHash values match if (clientSpawnedObject.GlobalObjectIdHash != spawnedObjectGlobalObjectIdHash) { From 0a56f44042eee75638d210990682dbcf768e4331 Mon Sep 17 00:00:00 2001 From: Emma Date: Fri, 29 May 2026 17:35:41 -0400 Subject: [PATCH 09/26] fix: Add more detailed logging around RPC messages (#3994) * fix: Add more detailed logging around RPC messages --- .../Runtime/Logging/ContextualLogger.cs | 17 +- .../Runtime/Logging/LogBuilder.cs | 1 + .../Runtime/Logging/LogContext.cs | 76 -------- .../Runtime/Logging/LogContext.meta | 3 + .../Logging/LogContext/CollectionContext.cs | 25 +++ .../LogContext/CollectionContext.cs.meta | 3 + .../{ => LogContext}/GenericContext.cs | 23 ++- .../{ => LogContext}/GenericContext.cs.meta | 0 .../Runtime/Logging/LogContext/LogContext.cs | 169 ++++++++++++++++++ .../{ => LogContext}/LogContext.cs.meta | 0 .../LogContext/LogContextNetworkBehaviour.cs | 21 +++ .../LogContextNetworkBehaviour.cs.meta | 3 + .../LogContextNetworkManager.cs | 0 .../LogContextNetworkManager.cs.meta | 0 .../LogContext/LogContextNetworkObject.cs | 21 +++ .../LogContextNetworkObject.cs.meta | 3 + .../Runtime/Messaging/Messages/RpcMessages.cs | 95 +++++----- .../Tests/Runtime/Rpc/RpcInvocationTests.cs | 11 +- 18 files changed, 335 insertions(+), 136 deletions(-) delete mode 100644 com.unity.netcode.gameobjects/Runtime/Logging/LogContext.cs create mode 100644 com.unity.netcode.gameobjects/Runtime/Logging/LogContext.meta create mode 100644 com.unity.netcode.gameobjects/Runtime/Logging/LogContext/CollectionContext.cs create mode 100644 com.unity.netcode.gameobjects/Runtime/Logging/LogContext/CollectionContext.cs.meta rename com.unity.netcode.gameobjects/Runtime/Logging/{ => LogContext}/GenericContext.cs (78%) rename com.unity.netcode.gameobjects/Runtime/Logging/{ => LogContext}/GenericContext.cs.meta (100%) create mode 100644 com.unity.netcode.gameobjects/Runtime/Logging/LogContext/LogContext.cs rename com.unity.netcode.gameobjects/Runtime/Logging/{ => LogContext}/LogContext.cs.meta (100%) create mode 100644 com.unity.netcode.gameobjects/Runtime/Logging/LogContext/LogContextNetworkBehaviour.cs create mode 100644 com.unity.netcode.gameobjects/Runtime/Logging/LogContext/LogContextNetworkBehaviour.cs.meta rename com.unity.netcode.gameobjects/Runtime/Logging/{ => LogContext}/LogContextNetworkManager.cs (100%) rename com.unity.netcode.gameobjects/Runtime/Logging/{ => LogContext}/LogContextNetworkManager.cs.meta (100%) create mode 100644 com.unity.netcode.gameobjects/Runtime/Logging/LogContext/LogContextNetworkObject.cs create mode 100644 com.unity.netcode.gameobjects/Runtime/Logging/LogContext/LogContextNetworkObject.cs.meta diff --git a/com.unity.netcode.gameobjects/Runtime/Logging/ContextualLogger.cs b/com.unity.netcode.gameobjects/Runtime/Logging/ContextualLogger.cs index 006f919ce1..3141a2a3bf 100644 --- a/com.unity.netcode.gameobjects/Runtime/Logging/ContextualLogger.cs +++ b/com.unity.netcode.gameobjects/Runtime/Logging/ContextualLogger.cs @@ -60,7 +60,7 @@ public ContextualLogger(Object inspectorObject, [NotNull] NetworkManager network } /// Used for the NetworkLog - internal ContextualLogger(NetworkManager networkManager, bool useCompatibilityMode) + internal ContextualLogger(NetworkManager networkManager, bool useCompatibilityMode = false) { m_UseCompatibilityMode = useCompatibilityMode; m_ManagerContext = new LogContextNetworkManager(networkManager); @@ -89,7 +89,7 @@ internal DisposableContext AddDisposableInfo(string key, object value) [Conditional(k_CompilationCondition)] internal void RemoveInfo(string key) { - m_LoggerContext.ClearInfo(key); + m_LoggerContext.RemoveInfo(key); } [HideInCallstack] @@ -125,6 +125,19 @@ public void Exception(Exception exception) { Debug.unityLogger.LogException(exception, m_Object); } + [HideInCallstack] + public void Exception(Exception exception, Context context) + { + // Don't act if the LogLevel is higher than the level of this log + if (m_ManagerContext.LogLevel > context.Level) + { + return; + } + + var message = BuildLog(context); + Debug.unityLogger.LogException(new Exception(message, exception), context.RelevantObjectOverride ?? m_Object); + } + [HideInCallstack] private void Log(LogType logType, Context context) diff --git a/com.unity.netcode.gameobjects/Runtime/Logging/LogBuilder.cs b/com.unity.netcode.gameobjects/Runtime/Logging/LogBuilder.cs index 0c59e10bd4..c8f8c1ba25 100644 --- a/com.unity.netcode.gameobjects/Runtime/Logging/LogBuilder.cs +++ b/com.unity.netcode.gameobjects/Runtime/Logging/LogBuilder.cs @@ -31,6 +31,7 @@ public void AppendInfo(object key, object value) } public void Append(string value) => m_Builder.Append(value); + public void AppendLine(string value) => m_Builder.AppendLine(value); public string Build() => m_Builder.ToString(); } diff --git a/com.unity.netcode.gameobjects/Runtime/Logging/LogContext.cs b/com.unity.netcode.gameobjects/Runtime/Logging/LogContext.cs deleted file mode 100644 index 8c6cbd0bf1..0000000000 --- a/com.unity.netcode.gameobjects/Runtime/Logging/LogContext.cs +++ /dev/null @@ -1,76 +0,0 @@ -using System.Runtime.CompilerServices; -using Object = UnityEngine.Object; - -namespace Unity.Netcode.Logging -{ - internal interface ILogContext - { - public void AppendTo(LogBuilder builder) - { - } - } - - internal struct Context : ILogContext - { - public readonly LogLevel Level; - private readonly string m_CallingFunction; - internal readonly string Message; - internal Object RelevantObjectOverride; - - private readonly GenericContext m_Other; - - public Context(LogLevel level, string msg, [CallerMemberName] string memberName = "") - { - Level = level; - Message = msg; - m_CallingFunction = memberName; - - m_Other = GenericContext.Create(); - RelevantObjectOverride = null; - } - - internal Context(LogLevel level, string msg, bool noCaller) - { - Level = level; - Message = msg; - m_CallingFunction = null; - - m_Other = GenericContext.Create(); - RelevantObjectOverride = null; - } - - public void AppendTo(LogBuilder builder) - { - // [CallingFunction] - if (!string.IsNullOrEmpty(m_CallingFunction)) - { - builder.AppendTag(m_CallingFunction); - } - - // [SomeContext][SomeName:SomeValue] - m_Other.AppendTo(builder); - - // Human-readable log message - builder.Append(" "); - builder.Append(Message); - } - - public Context AddInfo(object key, object value) - { - m_Other.StoreInfo(key, value); - return this; - } - - public Context AddTag(string msg) - { - m_Other.StoreTag(msg); - return this; - } - - public Context AddObject(Object obj) - { - RelevantObjectOverride = obj; - return this; - } - } -} diff --git a/com.unity.netcode.gameobjects/Runtime/Logging/LogContext.meta b/com.unity.netcode.gameobjects/Runtime/Logging/LogContext.meta new file mode 100644 index 0000000000..7c7f21c305 --- /dev/null +++ b/com.unity.netcode.gameobjects/Runtime/Logging/LogContext.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 1a57adef9bcf4768866c1e922dff4ad1 +timeCreated: 1779217887 \ No newline at end of file diff --git a/com.unity.netcode.gameobjects/Runtime/Logging/LogContext/CollectionContext.cs b/com.unity.netcode.gameobjects/Runtime/Logging/LogContext/CollectionContext.cs new file mode 100644 index 0000000000..70c493950d --- /dev/null +++ b/com.unity.netcode.gameobjects/Runtime/Logging/LogContext/CollectionContext.cs @@ -0,0 +1,25 @@ +using System.Collections.Generic; + +namespace Unity.Netcode.Logging +{ + internal delegate string LogCollectionBuilder(TItem item); + internal readonly struct CollectionContext : ILogContext + { + private readonly LogCollectionBuilder m_Delegate; + private readonly IEnumerable m_Collection; + + public CollectionContext(IEnumerable collection, LogCollectionBuilder builder) + { + m_Delegate = builder; + m_Collection = collection; + } + + public void AppendTo(LogBuilder builder) + { + foreach (var item in m_Collection) + { + builder.AppendLine(m_Delegate(item)); + } + } + } +} diff --git a/com.unity.netcode.gameobjects/Runtime/Logging/LogContext/CollectionContext.cs.meta b/com.unity.netcode.gameobjects/Runtime/Logging/LogContext/CollectionContext.cs.meta new file mode 100644 index 0000000000..5323a6eda2 --- /dev/null +++ b/com.unity.netcode.gameobjects/Runtime/Logging/LogContext/CollectionContext.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: f64de1e86c70433e96280f0c8af6e8f6 +timeCreated: 1779214517 \ No newline at end of file diff --git a/com.unity.netcode.gameobjects/Runtime/Logging/GenericContext.cs b/com.unity.netcode.gameobjects/Runtime/Logging/LogContext/GenericContext.cs similarity index 78% rename from com.unity.netcode.gameobjects/Runtime/Logging/GenericContext.cs rename to com.unity.netcode.gameobjects/Runtime/Logging/LogContext/GenericContext.cs index 47d198f447..643ec30324 100644 --- a/com.unity.netcode.gameobjects/Runtime/Logging/GenericContext.cs +++ b/com.unity.netcode.gameobjects/Runtime/Logging/LogContext/GenericContext.cs @@ -5,20 +5,20 @@ namespace Unity.Netcode.Logging { internal readonly struct GenericContext : ILogContext, IDisposable { - private readonly List m_Contexts; + private readonly List m_Tags; private readonly Dictionary m_Info; - private GenericContext(List contexts, Dictionary info) + private GenericContext(List tags, Dictionary info) { - m_Contexts = contexts; + m_Tags = tags; m_Info = info; } public void AppendTo(LogBuilder builder) { - if (m_Contexts != null) + if (m_Tags != null) { - foreach (var ctx in m_Contexts) + foreach (var ctx in m_Tags) { builder.AppendTag(ctx); } @@ -33,9 +33,9 @@ public void AppendTo(LogBuilder builder) } } - public void StoreTag(string msg) + public void StoreTag(string tag) { - m_Contexts.Add(msg); + m_Tags.Add(tag); } public void StoreInfo(object key, object value) @@ -43,11 +43,16 @@ public void StoreInfo(object key, object value) m_Info.Add(key, value); } - public void ClearInfo(object key) + public void RemoveInfo(object key) { m_Info?.Remove(key); } + public void RemoveTag(string tag) + { + m_Tags?.Remove(tag); + } + public void Dispose() { PreallocatedStore.Free(this); @@ -76,7 +81,7 @@ internal static GenericContext GetPreallocated() internal static void Free(GenericContext ctx) { - ctx.m_Contexts.Clear(); + ctx.m_Tags.Clear(); ctx.m_Info.Clear(); k_Preallocated.Enqueue(ctx); } diff --git a/com.unity.netcode.gameobjects/Runtime/Logging/GenericContext.cs.meta b/com.unity.netcode.gameobjects/Runtime/Logging/LogContext/GenericContext.cs.meta similarity index 100% rename from com.unity.netcode.gameobjects/Runtime/Logging/GenericContext.cs.meta rename to com.unity.netcode.gameobjects/Runtime/Logging/LogContext/GenericContext.cs.meta diff --git a/com.unity.netcode.gameobjects/Runtime/Logging/LogContext/LogContext.cs b/com.unity.netcode.gameobjects/Runtime/Logging/LogContext/LogContext.cs new file mode 100644 index 0000000000..a36b6b91fb --- /dev/null +++ b/com.unity.netcode.gameobjects/Runtime/Logging/LogContext/LogContext.cs @@ -0,0 +1,169 @@ +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using Object = UnityEngine.Object; + +namespace Unity.Netcode.Logging +{ + internal interface ILogContext + { + public void AppendTo(LogBuilder builder); + } + + internal struct Context : ILogContext, IDisposable + { + public readonly LogLevel Level; + private readonly string m_CallingFunction; + internal readonly string Message; + internal Object RelevantObjectOverride; + + private readonly GenericContext m_Other; + private List m_Prepend; + private List m_Postpend; + + public Context(LogLevel level, string msg, [CallerMemberName] string memberName = "") + { + Level = level; + Message = msg; + m_CallingFunction = memberName; + + m_Other = GenericContext.Create(); + RelevantObjectOverride = null; + m_Prepend = null; + m_Postpend = null; + } + + internal Context(LogLevel level, string msg, bool noCaller) + { + Level = level; + Message = msg; + m_CallingFunction = null; + + m_Other = GenericContext.Create(); + RelevantObjectOverride = null; + m_Prepend = null; + m_Postpend = null; + } + + public void AppendTo(LogBuilder builder) + { + // [CallingFunction] + if (!string.IsNullOrEmpty(m_CallingFunction)) + { + builder.AppendTag(m_CallingFunction); + } + + + // [SomeContext][SomeName:SomeValue] + m_Other.AppendTo(builder); + + if (m_Prepend != null) + { + foreach (var context in m_Prepend) + { + context.AppendTo(builder); + } + } + + // Human-readable log message + builder.Append(" "); + builder.Append(Message); + + if (m_Postpend != null) + { + foreach (var context in m_Postpend) + { + context.AppendTo(builder); + } + } + } + + public Context AddInfo(object key, object value) + { + m_Other.StoreInfo(key, value); + return this; + } + + public Context AddTag(string msg) + { + m_Other.StoreTag(msg); + return this; + } + + public Context AddObject(Object obj) + { + RelevantObjectOverride = obj; + return this; + } + + public Context AddNetworkObject(NetworkObject networkObject) + { + AddPrepend(new LogContextNetworkObject(networkObject)); + RelevantObjectOverride = networkObject; + return this; + } + + public Context AddNetworkBehaviour(NetworkBehaviour networkBehaviour) + { + AddPrepend(new LogContextNetworkBehaviour(networkBehaviour)); + RelevantObjectOverride = networkBehaviour; + return this; + } + + public Context AddCollection(IEnumerable collection, LogCollectionBuilder builder) + { + AddPostpend(new CollectionContext(collection, builder)); + return this; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void AddPrepend(ILogContext prepend) + { + if (m_Prepend == null) + { + m_Prepend = PreallocatedStore.GetPreallocated(); + } + m_Prepend.Add(prepend); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void AddPostpend(ILogContext postpend) + { + if (m_Postpend == null) + { + m_Postpend = PreallocatedStore.GetPreallocated(); + } + m_Postpend.Add(postpend); + } + + public void Dispose() + { + m_Other.Dispose(); + PreallocatedStore.Free(m_Prepend); + PreallocatedStore.Free(m_Postpend); + m_Prepend = null; + m_Postpend = null; + } + + private static class PreallocatedStore + { + private static readonly Queue> k_Preallocated = new(); + + internal static List GetPreallocated() + { + if (k_Preallocated.Count > 0) + { + k_Preallocated.Dequeue(); + } + + return new List(); + } + + internal static void Free(List collection) + { + collection.Clear(); + k_Preallocated.Enqueue(collection); + } + } + } +} diff --git a/com.unity.netcode.gameobjects/Runtime/Logging/LogContext.cs.meta b/com.unity.netcode.gameobjects/Runtime/Logging/LogContext/LogContext.cs.meta similarity index 100% rename from com.unity.netcode.gameobjects/Runtime/Logging/LogContext.cs.meta rename to com.unity.netcode.gameobjects/Runtime/Logging/LogContext/LogContext.cs.meta diff --git a/com.unity.netcode.gameobjects/Runtime/Logging/LogContext/LogContextNetworkBehaviour.cs b/com.unity.netcode.gameobjects/Runtime/Logging/LogContext/LogContextNetworkBehaviour.cs new file mode 100644 index 0000000000..149fd001ff --- /dev/null +++ b/com.unity.netcode.gameobjects/Runtime/Logging/LogContext/LogContextNetworkBehaviour.cs @@ -0,0 +1,21 @@ +namespace Unity.Netcode.Logging +{ + internal readonly struct LogContextNetworkBehaviour : ILogContext + { + private readonly NetworkBehaviour m_NetworkBehaviour; + + public LogContextNetworkBehaviour(NetworkBehaviour networkBehaviour) + { + m_NetworkBehaviour = networkBehaviour; + } + + public void AppendTo(LogBuilder builder) + { + builder.AppendTag(m_NetworkBehaviour.gameObject.name); + if (m_NetworkBehaviour.IsSpawned) + { + builder.AppendInfo(nameof(NetworkBehaviour.NetworkBehaviourId), m_NetworkBehaviour.NetworkBehaviourId); + } + } + } +} diff --git a/com.unity.netcode.gameobjects/Runtime/Logging/LogContext/LogContextNetworkBehaviour.cs.meta b/com.unity.netcode.gameobjects/Runtime/Logging/LogContext/LogContextNetworkBehaviour.cs.meta new file mode 100644 index 0000000000..a4f9cae7ed --- /dev/null +++ b/com.unity.netcode.gameobjects/Runtime/Logging/LogContext/LogContextNetworkBehaviour.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: ce875bd203c24cf1b916741eac3a55f8 +timeCreated: 1779217913 \ No newline at end of file diff --git a/com.unity.netcode.gameobjects/Runtime/Logging/LogContextNetworkManager.cs b/com.unity.netcode.gameobjects/Runtime/Logging/LogContext/LogContextNetworkManager.cs similarity index 100% rename from com.unity.netcode.gameobjects/Runtime/Logging/LogContextNetworkManager.cs rename to com.unity.netcode.gameobjects/Runtime/Logging/LogContext/LogContextNetworkManager.cs diff --git a/com.unity.netcode.gameobjects/Runtime/Logging/LogContextNetworkManager.cs.meta b/com.unity.netcode.gameobjects/Runtime/Logging/LogContext/LogContextNetworkManager.cs.meta similarity index 100% rename from com.unity.netcode.gameobjects/Runtime/Logging/LogContextNetworkManager.cs.meta rename to com.unity.netcode.gameobjects/Runtime/Logging/LogContext/LogContextNetworkManager.cs.meta diff --git a/com.unity.netcode.gameobjects/Runtime/Logging/LogContext/LogContextNetworkObject.cs b/com.unity.netcode.gameobjects/Runtime/Logging/LogContext/LogContextNetworkObject.cs new file mode 100644 index 0000000000..d7feb201b1 --- /dev/null +++ b/com.unity.netcode.gameobjects/Runtime/Logging/LogContext/LogContextNetworkObject.cs @@ -0,0 +1,21 @@ +namespace Unity.Netcode.Logging +{ + internal readonly struct LogContextNetworkObject : ILogContext + { + private readonly NetworkObject m_NetworkObject; + + public LogContextNetworkObject(NetworkObject networkObject) + { + m_NetworkObject = networkObject; + } + + public void AppendTo(LogBuilder builder) + { + builder.AppendTag(m_NetworkObject.name); + if (m_NetworkObject.IsSpawned) + { + builder.AppendInfo(nameof(NetworkObject.NetworkObjectId), m_NetworkObject.NetworkObjectId); + } + } + } +} diff --git a/com.unity.netcode.gameobjects/Runtime/Logging/LogContext/LogContextNetworkObject.cs.meta b/com.unity.netcode.gameobjects/Runtime/Logging/LogContext/LogContextNetworkObject.cs.meta new file mode 100644 index 0000000000..9c98d47b80 --- /dev/null +++ b/com.unity.netcode.gameobjects/Runtime/Logging/LogContext/LogContextNetworkObject.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: d6f400599d664e92a747913ca672e02e +timeCreated: 1779217114 \ No newline at end of file diff --git a/com.unity.netcode.gameobjects/Runtime/Messaging/Messages/RpcMessages.cs b/com.unity.netcode.gameobjects/Runtime/Messaging/Messages/RpcMessages.cs index c8be0c251d..843d875216 100644 --- a/com.unity.netcode.gameobjects/Runtime/Messaging/Messages/RpcMessages.cs +++ b/com.unity.netcode.gameobjects/Runtime/Messaging/Messages/RpcMessages.cs @@ -1,6 +1,6 @@ using System; using Unity.Collections; -using UnityEngine; +using Unity.Netcode.Logging; namespace Unity.Netcode { @@ -16,80 +16,79 @@ public static unsafe void Serialize(ref FastBufferWriter writer, ref RpcMetadata public static unsafe bool Deserialize(ref FastBufferReader reader, ref NetworkContext context, ref RpcMetadata metadata, ref FastBufferReader payload, string messageType) { + var networkManager = (NetworkManager)context.SystemOwner; ByteUnpacker.ReadValueBitPacked(reader, out metadata.NetworkObjectId); - ByteUnpacker.ReadValueBitPacked(reader, out metadata.NetworkBehaviourId); - ByteUnpacker.ReadValueBitPacked(reader, out metadata.NetworkRpcMethodId); - var networkManager = (NetworkManager)context.SystemOwner; - if (!networkManager.SpawnManager.SpawnedObjects.ContainsKey(metadata.NetworkObjectId)) + if (!networkManager.SpawnManager.SpawnedObjects.TryGetValue(metadata.NetworkObjectId, out var networkObject)) { + networkManager.Log.Info(new Context(LogLevel.Developer, $"Received RPC message for {nameof(NetworkObject)} that doesn't exist yet. Deferring the message.").AddInfo("SenderClientId", context.SenderId).AddInfo(nameof(NetworkObject.NetworkObjectId), metadata.NetworkObjectId).AddInfo(nameof(RpcMetadata.NetworkRpcMethodId), metadata.NetworkRpcMethodId)); networkManager.DeferredMessageManager.DeferMessage(IDeferredNetworkMessageManager.TriggerType.OnSpawn, metadata.NetworkObjectId, reader, ref context, messageType); return false; } - var networkObject = networkManager.SpawnManager.SpawnedObjects[metadata.NetworkObjectId]; - var networkBehaviour = networkManager.SpawnManager.SpawnedObjects[metadata.NetworkObjectId].GetNetworkBehaviourAtOrderIndex(metadata.NetworkBehaviourId); - if (networkBehaviour == null) - { - return false; - } - - if (!NetworkBehaviour.__rpc_func_table[networkBehaviour.GetType()].ContainsKey(metadata.NetworkRpcMethodId)) - { - return false; - } + ByteUnpacker.ReadValueBitPacked(reader, out metadata.NetworkBehaviourId); + ByteUnpacker.ReadValueBitPacked(reader, out metadata.NetworkRpcMethodId); payload = new FastBufferReader(reader.GetUnsafePtrAtCurrentPosition(), Allocator.None, reader.Length - reader.Position); -#if MULTIPLAYER_TOOLS && (DEVELOPMENT_BUILD || UNITY_EDITOR || UNITY_MP_TOOLS_NET_STATS_MONITOR_ENABLED_IN_RELEASE) - networkBehaviour.TrackRpcMetricsReceive(ref metadata, ref context, reader.Length); -#endif return true; } public static void Handle(ref NetworkContext context, ref RpcMetadata metadata, ref FastBufferReader payload, ref __RpcParams rpcParams) { var networkManager = (NetworkManager)context.SystemOwner; + if (!networkManager.SpawnManager.SpawnedObjects.TryGetValue(metadata.NetworkObjectId, out var networkObject)) { // If the NetworkObject no longer exists then just log a warning when developer mode logging is enabled and exit. // This can happen if NetworkObject is despawned and a client sends an RPC before receiving the despawn message. - if (networkManager.LogLevel == LogLevel.Developer) - { - NetworkLog.LogWarning($"[{metadata.NetworkObjectId}, {metadata.NetworkBehaviourId}, {metadata.NetworkRpcMethodId}] An RPC called on a {nameof(NetworkObject)} that is not in the spawned objects list. Please make sure the {nameof(NetworkObject)} is spawned before calling RPCs."); - } - + networkManager.Log.Warning(new Context(LogLevel.Developer, $"Received RPC message for {nameof(NetworkObject)} that doesn't exist yet. Deferring the message.").AddInfo(nameof(NetworkObject.NetworkObjectId), metadata.NetworkObjectId).AddInfo(nameof(RpcMetadata.NetworkRpcMethodId), metadata.NetworkRpcMethodId)); return; } var networkBehaviour = networkObject.GetNetworkBehaviourAtOrderIndex(metadata.NetworkBehaviourId); - try + if (networkBehaviour == null) + { + networkManager.Log.Error(new Context(LogLevel.Normal, $"Received RPC message for {nameof(NetworkBehaviour)} that doesn't exist. Dropping RPC message").AddNetworkObject(networkObject).AddInfo(nameof(NetworkBehaviour.NetworkBehaviourId), networkBehaviour.NetworkBehaviourId)); + return; + } + + var type = networkBehaviour.GetType(); + if (!NetworkBehaviour.__rpc_func_table.TryGetValue(type, out var rpcsForBehaviour) || !NetworkBehaviour.__rpc_permission_table.TryGetValue(type, out var permissionsTable)) + { + networkManager.Log.Error(new Context(LogLevel.Normal, $"Rpc table doesn't have RPCs registered for this {nameof(NetworkBehaviour)}. Dropping RPC message").AddNetworkObject(networkObject).AddInfo(nameof(NetworkBehaviour.NetworkBehaviourId), networkBehaviour.NetworkBehaviourId).AddInfo(nameof(NetworkBehaviour), type)); + return; + } + if (!rpcsForBehaviour.TryGetValue(metadata.NetworkRpcMethodId, out var receiveHandler) || !permissionsTable.TryGetValue(metadata.NetworkRpcMethodId, out var permission)) { - var permission = NetworkBehaviour.__rpc_permission_table[networkBehaviour.GetType()][metadata.NetworkRpcMethodId]; + networkManager.Log.Error(new Context(LogLevel.Normal, "Received RPC message for RPC receiver that doesn't exist. Dropping RPC message").AddNetworkBehaviour(networkBehaviour).AddInfo(nameof(RpcMetadata.NetworkRpcMethodId), metadata.NetworkRpcMethodId)); + return; + } - if ((permission == RpcInvokePermission.Server && rpcParams.SenderId != NetworkManager.ServerClientId) || - (permission == RpcInvokePermission.Owner && rpcParams.SenderId != networkObject.OwnerClientId)) - { - if (networkManager.LogLevel <= LogLevel.Developer) - { - NetworkLog.LogErrorServer($"Rpc message received from client-{rpcParams.SenderId} who does not have permission to perform this operation!"); - } - return; - } + if ((permission == RpcInvokePermission.Server && rpcParams.SenderId != NetworkManager.ServerClientId) || + (permission == RpcInvokePermission.Owner && rpcParams.SenderId != networkObject.OwnerClientId)) + { + networkManager.Log.ErrorServer(new Context(LogLevel.Normal, "Rpc message received from a client without permission to perform this operation!. Dropping RPC message").AddNetworkBehaviour(networkBehaviour)); + return; + } + +#if MULTIPLAYER_TOOLS && (DEVELOPMENT_BUILD || UNITY_EDITOR || UNITY_MP_TOOLS_NET_STATS_MONITOR_ENABLED_IN_RELEASE) + networkBehaviour.TrackRpcMetricsReceive(ref metadata, ref context, payload.Length); +#endif - NetworkBehaviour.__rpc_func_table[networkBehaviour.GetType()][metadata.NetworkRpcMethodId](networkBehaviour, payload, rpcParams); + try + { + receiveHandler(networkBehaviour, payload, rpcParams); } catch (Exception ex) { - Debug.LogException(new Exception($"Unhandled RPC exception!", ex)); - if (networkManager.LogLevel <= LogLevel.Developer) + networkManager.Log.Exception(ex, new Context(LogLevel.Error, "Unhandled RPC exception!").AddNetworkBehaviour(networkBehaviour)); + + var methodId = metadata.NetworkRpcMethodId; + networkManager.Log.Info(new Context(LogLevel.Developer, "RPC Table Contents").AddCollection(rpcsForBehaviour, entry => { - Debug.Log($"RPC Table Contents"); - foreach (var entry in NetworkBehaviour.__rpc_func_table[networkBehaviour.GetType()]) - { - var permission = NetworkBehaviour.__rpc_permission_table[networkBehaviour.GetType()][metadata.NetworkRpcMethodId]; - Debug.Log($"{entry.Key} | {entry.Value.Method.Name} | {permission}"); - } - } + var invokePermission = NetworkBehaviour.__rpc_permission_table[networkBehaviour.GetType()][methodId]; + return $"{entry.Key} | {entry.Value.Method.Name} | {invokePermission}"; + })); } } } @@ -270,12 +269,12 @@ public void Handle(ref NetworkContext context) } catch (Exception ex) { - Debug.LogException(ex); + networkManager.Log.Exception(ex); } } else { - NetworkLog.LogErrorServer($"Received {nameof(ForwardServerRpcMessage)} on client-{networkManager.LocalClientId}! Only DAHost may forward RPC messages!"); + networkManager.Log.ErrorServer(new Context(LogLevel.Error, $"Received {nameof(ForwardServerRpcMessage)} when not the DAHost! Only DAHost may forward RPC messages!").AddInfo("SenderClientId", context.SenderId)); } ServerRpcMessage.ReadBuffer.Dispose(); ServerRpcMessage.WriteBuffer.Dispose(); @@ -351,7 +350,7 @@ public void Handle(ref NetworkContext context) } else { - NetworkLog.LogErrorServer($"Received {nameof(ForwardClientRpcMessage)} on client-{networkManager.LocalClientId}! Only DAHost may forward RPC messages!"); + networkManager.Log.ErrorServer(new Context(LogLevel.Error, $"Received {nameof(ForwardClientRpcMessage)} when not the DAHost! Only DAHost may forward RPC messages!").AddInfo("SenderClientId", context.SenderId)); } ClientRpcMessage.WriteBuffer.Dispose(); ClientRpcMessage.ReadBuffer.Dispose(); diff --git a/com.unity.netcode.gameobjects/Tests/Runtime/Rpc/RpcInvocationTests.cs b/com.unity.netcode.gameobjects/Tests/Runtime/Rpc/RpcInvocationTests.cs index 996021d425..613b81f5e5 100644 --- a/com.unity.netcode.gameobjects/Tests/Runtime/Rpc/RpcInvocationTests.cs +++ b/com.unity.netcode.gameobjects/Tests/Runtime/Rpc/RpcInvocationTests.cs @@ -139,7 +139,16 @@ public IEnumerator RpcInvokePermissionSendingTests() [UnityTest] public IEnumerator RpcInvokePermissionReceivingTests() { - var firstClient = GetNonAuthorityNetworkManager(0); + NetworkManager firstClient = null; + foreach (var networkManager in m_NetworkManagers) + { + if (firstClient == null && !networkManager.IsServer && !networkManager.LocalClient.IsSessionOwner) + { + firstClient = networkManager; + } + + networkManager.LogLevel = LogLevel.Error; + } var spawnedObject = SpawnObject(m_Prefab, firstClient).GetComponent(); From 7d03b41b1152d819b9132dbea88e6576dcde1416 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Noellie=20V=C3=A9lez?= Date: Mon, 1 Jun 2026 14:29:15 +0200 Subject: [PATCH 10/26] refactor: Logging update (#4002) Update RigidbodyContactEventManager to use new logging system --- .../Runtime/Components/RigidbodyContactEventManager.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/com.unity.netcode.gameobjects/Runtime/Components/RigidbodyContactEventManager.cs b/com.unity.netcode.gameobjects/Runtime/Components/RigidbodyContactEventManager.cs index bf54c4d964..63031d2a24 100644 --- a/com.unity.netcode.gameobjects/Runtime/Components/RigidbodyContactEventManager.cs +++ b/com.unity.netcode.gameobjects/Runtime/Components/RigidbodyContactEventManager.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using Unity.Collections; using Unity.Jobs; +using Unity.Netcode.Logging; using Unity.Netcode.Runtime; using UnityEngine; @@ -105,14 +106,16 @@ private struct JobResultStruct private readonly Dictionary m_HandlerInfo = new Dictionary(); #endif + private ContextualLogger m_Log; private void OnEnable() { + m_Log = new ContextualLogger(this); m_ResultsArray = new NativeArray(16, Allocator.Persistent); Physics.ContactEvent += Physics_ContactEvent; if (Instance != null) { - NetworkLog.LogError($"[Invalid][Multiple Instances] Found more than one instance of {nameof(RigidbodyContactEventManager)}: {name} and {Instance.name}"); - NetworkLog.LogError($"[Disable][Additional Instance] Disabling {name} instance!"); + m_Log.Error(new Context(LogLevel.Error, $"Found more than one instance of {nameof(RigidbodyContactEventManager)}").AddTag("Invalid").AddTag("Multiple Instances").AddInfo("Instance 1", Instance.name).AddInfo("Instance 2", name)); + m_Log.Error(new Context(LogLevel.Error, $"Disabling instance: ").AddTag("Disable").AddTag("Additional Instance").AddInfo("Instance", name)); gameObject.SetActive(false); return; } From c48f54d94f9828564e4fa3b2ddeaabd0f78e4cd2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Noellie=20V=C3=A9lez?= Date: Mon, 1 Jun 2026 23:09:12 +0200 Subject: [PATCH 11/26] fix: reset static fields for Fast Enter Play Mode (#3956) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: reset static fields for Fast Enter Play Mode support   - Added ResetStaticsOnLoad where possible   - Applied [AutoStaticsCleanup] to generic classes when applicable   - Promoted immutable statics to readonly or const   - Added OnDisable to deregister event subscriptions - chore: disable domain reload and remove treat warnings as errors in test project settings - test: add NetworkManager singleton reset test for Fast Enter Play Mode * fix: add missing message type registrations in MessageDelivery * chore: cleanup   - Remove unused m_ParentedChildren field and related loops in NetworkTransform   - Remove unused m_DespawnedInSceneObjects field in SceneEventData   - Replace static quaternion scratch buffer in QuaternionCompressor with local variables   - Consolidate LogSerializationOrder and EnableSerializationLogs into LogConfiguration - Update summary comments * docs: update CHANGELOG --------- Co-authored-by: Noel Stephens Co-authored-by: Emma --- com.unity.netcode.gameobjects/CHANGELOG.md | 1 + .../Runtime/Components/NetworkAnimator.cs | 1 - .../Runtime/Components/NetworkTransform.cs | 51 ++++++------------- .../Components/QuaternionCompressor.cs | 48 +++++++---------- .../RigidbodyContactEventManager.cs | 4 +- .../Configuration/CommandLineOptions.cs | 43 +++++++++------- .../Runtime/Core/ComponentFactory.cs | 4 ++ .../Runtime/Core/NetworkBehaviour.cs | 2 - .../Runtime/Core/NetworkManager.cs | 19 ++++++- .../Runtime/Core/NetworkObject.cs | 6 +++ .../Runtime/Core/NetworkUpdateLoop.cs | 37 +++++++++----- .../Runtime/Logging/NetworkLog.cs | 10 +++- .../Messaging/DeferredMessageManager.cs | 4 ++ .../Runtime/Messaging/ILPPMessageProvider.cs | 4 ++ .../Runtime/Messaging/INetworkMessage.cs | 7 +-- .../Runtime/Messaging/MessageDelivery.cs | 29 +++++------ .../Messaging/NetworkMessageManager.cs | 16 +++--- .../Runtime/Metrics/NetworkMetrics.cs | 20 +++----- .../CollectionSerializationUtility.cs | 20 ++++---- .../NetworkVariableSerialization.cs | 2 + .../UserNetworkVariableSerialization.cs | 5 +- .../DefaultSceneManagerHandler.cs | 2 +- .../SceneManagement/NetworkSceneManager.cs | 15 +++++- .../Runtime/SceneManagement/SceneEventData.cs | 15 +++--- .../Runtime/Serialization/FastBufferWriter.cs | 8 +-- .../NetworkBehaviourReference.cs | 7 ++- .../Serialization/NetworkObjectReference.cs | 8 +-- .../SinglePlayer/SinglePlayerTransport.cs | 6 ++- .../Runtime/Transports/UTP/UnityTransport.cs | 21 ++++++-- .../TestHelpers/NetcodeIntegrationTest.cs | 2 - .../ProjectSettings/EditorSettings.asset | 2 +- .../ProjectSettings/ProjectSettings.asset | 3 +- 32 files changed, 238 insertions(+), 184 deletions(-) diff --git a/com.unity.netcode.gameobjects/CHANGELOG.md b/com.unity.netcode.gameobjects/CHANGELOG.md index 186d442d32..0a4aa4fde3 100644 --- a/com.unity.netcode.gameobjects/CHANGELOG.md +++ b/com.unity.netcode.gameobjects/CHANGELOG.md @@ -10,6 +10,7 @@ Additional documentation and release notes are available at [Multiplayer Documen ### Added +- Added support for Unity's Fast Enter Play Mode with domain reload disabled. (#3956) ### Changed diff --git a/com.unity.netcode.gameobjects/Runtime/Components/NetworkAnimator.cs b/com.unity.netcode.gameobjects/Runtime/Components/NetworkAnimator.cs index 7d82872890..3fb8166408 100644 --- a/com.unity.netcode.gameobjects/Runtime/Components/NetworkAnimator.cs +++ b/com.unity.netcode.gameobjects/Runtime/Components/NetworkAnimator.cs @@ -706,7 +706,6 @@ protected virtual bool OnIsServerAuthoritative() private int[] m_TransitionHash; private int[] m_AnimationHash; private float[] m_LayerWeights; - private static byte[] s_EmptyArray = new byte[] { }; private List m_ParametersToUpdate; private RpcParams m_RpcParams; private IGroupRpcTarget m_TargetGroup; diff --git a/com.unity.netcode.gameobjects/Runtime/Components/NetworkTransform.cs b/com.unity.netcode.gameobjects/Runtime/Components/NetworkTransform.cs index 0259138b46..18fd9cd88f 100644 --- a/com.unity.netcode.gameobjects/Runtime/Components/NetworkTransform.cs +++ b/com.unity.netcode.gameobjects/Runtime/Components/NetworkTransform.cs @@ -24,6 +24,18 @@ public class NetworkTransform : NetworkBehaviour [HideInInspector] [SerializeField] internal bool NetworkTransformExpanded; + + [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)] + private static void ResetStaticsOnLoad() + { + CurrentTick = 0; + TrackStateUpdateId = false; + AssignDefaultInterpolationType = false; + DefaultInterpolationType = default; + s_NetworkTickRegistration = new Dictionary(); + InterpolationBufferTickOffset = 0; + s_TickSynchPosition = 0; + } #endif internal enum Axis { X, Y, Z } @@ -1361,7 +1373,7 @@ public enum AuthorityModes /// /// When set each state update will contain a state identifier /// - internal static bool TrackStateUpdateId = false; + internal static bool TrackStateUpdateId; /// /// Enabled by default. @@ -2062,19 +2074,6 @@ private void TryCommitTransform(bool synchronize = false, bool settingState = fa childNetworkTransform.OnNetworkTick(true); } } - - // Synchronize any parented children with the parent's motion - foreach (var child in m_ParentedChildren) - { - // Synchronize any nested NetworkTransforms of the child with the parent's - foreach (var childNetworkTransform in child.NetworkTransforms) - { - if (childNetworkTransform.CanCommitToTransform) - { - childNetworkTransform.OnNetworkTick(true); - } - } - } } } } @@ -2221,13 +2220,11 @@ private bool CheckForStateChange(ref NetworkTransformState networkState, bool is // values are applied. var hasParentNetworkObject = false; - var parentNetworkObject = (NetworkObject)null; - // If the NetworkObject belonging to this NetworkTransform instance has a parent // (i.e. this handles nested NetworkTransforms under a parent at some layer above) if (NetworkObject.transform.parent != null) { - parentNetworkObject = NetworkObject.transform.parent.GetComponent(); + var parentNetworkObject = NetworkObject.transform.parent.GetComponent(); // In-scene placed NetworkObjects parented under a GameObject with no // NetworkObject preserve their lossyScale when synchronizing. @@ -3363,19 +3360,6 @@ private void OnNetworkStateChanged(NetworkTransformState oldState, NetworkTransf childNetworkTransform.OnNetworkTick(true); } } - - // Synchronize any parented children with the parent's motion - foreach (var child in m_ParentedChildren) - { - // Synchronize any nested NetworkTransforms of the child with the parent's - foreach (var childNetworkTransform in child.NetworkTransforms) - { - if (childNetworkTransform.CanCommitToTransform) - { - childNetworkTransform.OnNetworkTick(true); - } - } - } } // Provide notifications when the state has been updated @@ -3634,8 +3618,6 @@ internal override void InternalOnNetworkPreSpawn(ref NetworkManager networkManag /// public override void OnNetworkSpawn() { - m_ParentedChildren.Clear(); - Initialize(); if (CanCommitToTransform && !SwitchTransformSpaceWhenParented) @@ -3647,7 +3629,6 @@ public override void OnNetworkSpawn() private void CleanUpOnDestroyOrDespawn() { - m_ParentedChildren.Clear(); #if COM_UNITY_MODULES_PHYSICS || COM_UNITY_MODULES_PHYSICS2D var forUpdate = !m_UseRigidbodyForMotion; #else @@ -3861,7 +3842,6 @@ protected override void OnOwnershipChanged(ulong previous, ulong current) } internal bool IsNested; - private List m_ParentedChildren = new List(); private bool m_IsFirstNetworkTransform; @@ -4694,7 +4674,7 @@ internal static void UpdateNetworkTick(NetworkManager networkManager) /// The default value is 1 tick (plus the tick latency). When running on a local network, reducing this to 0 is recommended.
/// /// - public static int InterpolationBufferTickOffset = 0; + public static int InterpolationBufferTickOffset; internal static float GetTickLatency(NetworkManager networkManager) { if (networkManager.IsListening) @@ -4799,6 +4779,7 @@ public NetworkTransformTickRegistration(NetworkManager networkManager) } } } + private static int s_TickSynchPosition; private int m_NextTickSync; diff --git a/com.unity.netcode.gameobjects/Runtime/Components/QuaternionCompressor.cs b/com.unity.netcode.gameobjects/Runtime/Components/QuaternionCompressor.cs index f4d9b25dd2..3c338a2a84 100644 --- a/com.unity.netcode.gameobjects/Runtime/Components/QuaternionCompressor.cs +++ b/com.unity.netcode.gameobjects/Runtime/Components/QuaternionCompressor.cs @@ -4,7 +4,7 @@ namespace Unity.Netcode { /// - /// A Smallest Three Quaternion Compressor Implementation + /// The Smallest Three Quaternion Compressor Implementation /// /// /// Explanation of why "The smallest three": @@ -26,14 +26,14 @@ public static class QuaternionCompressor // We can further improve the encoding compression by dividing k_SqrtTwoOverTwo into 1.0f and multiplying that // by the precision mask (minor reduction of runtime calculations) - private const float k_CompressionEcodingMask = (1.0f / k_SqrtTwoOverTwoEncoding) * k_PrecisionMask; + private const float k_CompressionEncodingMask = (1.0f / k_SqrtTwoOverTwoEncoding) * k_PrecisionMask; // Used to shift the negative bit to the 10th bit position when compressing and encoding private const ushort k_ShiftNegativeBit = 9; // We can do the same for our decoding and decompression by dividing k_PrecisionMask into 1.0 and multiplying // that by k_SqrtTwoOverTwo (minor reduction of runtime calculations) - private const float k_DcompressionDecodingMask = (1.0f / k_PrecisionMask) * k_SqrtTwoOverTwoEncoding; + private const float k_DecompressionDecodingMask = (1.0f / k_PrecisionMask) * k_SqrtTwoOverTwoEncoding; // The sign bit position (10th bit) used when decompressing and decoding private const ushort k_NegShortBit = 0x200; @@ -42,9 +42,6 @@ public static class QuaternionCompressor private const ushort k_True = 1; private const ushort k_False = 0; - // Used to store the absolute value of the 4 quaternion elements - private static Quaternion s_QuatAbsValues = Quaternion.identity; - /// /// Compresses a Quaternion into an unsigned integer /// @@ -54,38 +51,31 @@ public static class QuaternionCompressor public static uint CompressQuaternion(ref Quaternion quaternion) { // Store off the absolute value for each Quaternion element - s_QuatAbsValues[0] = Mathf.Abs(quaternion[0]); - s_QuatAbsValues[1] = Mathf.Abs(quaternion[1]); - s_QuatAbsValues[2] = Mathf.Abs(quaternion[2]); - s_QuatAbsValues[3] = Mathf.Abs(quaternion[3]); + var quatAbsValue0 = Mathf.Abs(quaternion[0]); + var quatAbsValue1 = Mathf.Abs(quaternion[1]); + var quatAbsValue2 = Mathf.Abs(quaternion[2]); + var quatAbsValue3 = Mathf.Abs(quaternion[3]); // Get the largest element value of the quaternion to know what the remaining "Smallest Three" values are - var quatMax = Mathf.Max(s_QuatAbsValues[0], s_QuatAbsValues[1], s_QuatAbsValues[2], s_QuatAbsValues[3]); + var quatMax = Mathf.Max(quatAbsValue0, quatAbsValue1, quatAbsValue2, quatAbsValue3); - // Find the index of the largest element so we can skip that element while compressing and decompressing - var indexToSkip = (ushort)(s_QuatAbsValues[0] == quatMax ? 0 : s_QuatAbsValues[1] == quatMax ? 1 : s_QuatAbsValues[2] == quatMax ? 2 : 3); + // Find the index of the largest element, so we can skip that element while compressing and decompressing + var indexToSkip = (ushort)(quatAbsValue0 == quatMax ? 0 : quatAbsValue1 == quatMax ? 1 : quatAbsValue2 == quatMax ? 2 : 3); // Get the sign of the largest element which is all that is needed when calculating the sum of squares of a normalized quaternion. - var quatMaxSign = (quaternion[indexToSkip] < 0 ? k_True : k_False); // Start with the index to skip which will be shifted to the highest two bits var compressed = (uint)indexToSkip; - // Step 1: Start with the first element - var currentIndex = 0; - - // Step 2: If we are on the index to skip preserve the current compressed value, otherwise proceed to step 3 and 4 - // Step 3: Get the sign of the element we are processing. If it is the not the same as the largest value's sign bit then we set the bit - // Step 4: Get the compressed and encoded value by multiplying the absolute value of the current element by k_CompressionEcodingMask and round that result up - compressed = currentIndex != indexToSkip ? (compressed << 10) | (uint)((quaternion[currentIndex] < 0 ? k_True : k_False) != quatMaxSign ? k_True : k_False) << k_ShiftNegativeBit | (ushort)Mathf.Round(k_CompressionEcodingMask * s_QuatAbsValues[currentIndex]) : compressed; - currentIndex++; - // Repeat the last 3 steps for the remaining elements - compressed = currentIndex != indexToSkip ? (compressed << 10) | (uint)((quaternion[currentIndex] < 0 ? k_True : k_False) != quatMaxSign ? k_True : k_False) << k_ShiftNegativeBit | (ushort)Mathf.Round(k_CompressionEcodingMask * s_QuatAbsValues[currentIndex]) : compressed; - currentIndex++; - compressed = currentIndex != indexToSkip ? (compressed << 10) | (uint)((quaternion[currentIndex] < 0 ? k_True : k_False) != quatMaxSign ? k_True : k_False) << k_ShiftNegativeBit | (ushort)Mathf.Round(k_CompressionEcodingMask * s_QuatAbsValues[currentIndex]) : compressed; - currentIndex++; - compressed = currentIndex != indexToSkip ? (compressed << 10) | (uint)((quaternion[currentIndex] < 0 ? k_True : k_False) != quatMaxSign ? k_True : k_False) << k_ShiftNegativeBit | (ushort)Mathf.Round(k_CompressionEcodingMask * s_QuatAbsValues[currentIndex]) : compressed; + // Step 1: If we are on the index to skip, preserve the current compressed value, otherwise proceed to step 2 and 3 + // Step 2: Get the sign of the element we are processing. If it is not the same as the largest value's sign bit then we set the bit + // Step 3: Get the compressed and encoded value by multiplying the absolute value of the current element by k_CompressionEncodingMask and round that result up + compressed = 0 != indexToSkip ? (compressed << 10) | (uint)((quaternion[0] < 0 ? k_True : k_False) != quatMaxSign ? k_True : k_False) << k_ShiftNegativeBit | (ushort)Mathf.Round(k_CompressionEncodingMask * quatAbsValue0) : compressed; + // Repeat the 3 steps for the remaining elements + compressed = 1 != indexToSkip ? (compressed << 10) | (uint)((quaternion[1] < 0 ? k_True : k_False) != quatMaxSign ? k_True : k_False) << k_ShiftNegativeBit | (ushort)Mathf.Round(k_CompressionEncodingMask * quatAbsValue1) : compressed; + compressed = 2 != indexToSkip ? (compressed << 10) | (uint)((quaternion[2] < 0 ? k_True : k_False) != quatMaxSign ? k_True : k_False) << k_ShiftNegativeBit | (ushort)Mathf.Round(k_CompressionEncodingMask * quatAbsValue2) : compressed; + compressed = 3 != indexToSkip ? (compressed << 10) | (uint)((quaternion[3] < 0 ? k_True : k_False) != quatMaxSign ? k_True : k_False) << k_ShiftNegativeBit | (ushort)Mathf.Round(k_CompressionEncodingMask * quatAbsValue3) : compressed; // Return the compress quaternion return compressed; @@ -111,7 +101,7 @@ public static void DecompressQuaternion(ref Quaternion quaternion, uint compress continue; } // Check the negative bit and multiply that result with the decompressed and decoded value - quaternion[i] = ((compressed & k_NegShortBit) > 0 ? -1.0f : 1.0f) * ((compressed & k_PrecisionMask) * k_DcompressionDecodingMask); + quaternion[i] = ((compressed & k_NegShortBit) > 0 ? -1.0f : 1.0f) * ((compressed & k_PrecisionMask) * k_DecompressionDecodingMask); sumOfSquaredMagnitudes += quaternion[i] * quaternion[i]; compressed = compressed >> 10; } diff --git a/com.unity.netcode.gameobjects/Runtime/Components/RigidbodyContactEventManager.cs b/com.unity.netcode.gameobjects/Runtime/Components/RigidbodyContactEventManager.cs index 63031d2a24..3c94eae424 100644 --- a/com.unity.netcode.gameobjects/Runtime/Components/RigidbodyContactEventManager.cs +++ b/com.unity.netcode.gameobjects/Runtime/Components/RigidbodyContactEventManager.cs @@ -21,7 +21,7 @@ public struct ContactEventHandlerInfo ///
public bool ProvideNonRigidBodyContactEvents; /// - /// When set to true, the will prioritize invoking

+ /// When set to true, the will prioritize invoking
/// if it is the 2nd colliding body in the contact pair being processed. With distributed authority, setting this value to true when a is owned by the local client
/// will assure is only invoked on the authoritative side. ///
@@ -112,7 +112,7 @@ private void OnEnable() m_Log = new ContextualLogger(this); m_ResultsArray = new NativeArray(16, Allocator.Persistent); Physics.ContactEvent += Physics_ContactEvent; - if (Instance != null) + if (Instance != null && Instance != this) { m_Log.Error(new Context(LogLevel.Error, $"Found more than one instance of {nameof(RigidbodyContactEventManager)}").AddTag("Invalid").AddTag("Multiple Instances").AddInfo("Instance 1", Instance.name).AddInfo("Instance 2", name)); m_Log.Error(new Context(LogLevel.Error, $"Disabling instance: ").AddTag("Disable").AddTag("Additional Instance").AddInfo("Instance", name)); diff --git a/com.unity.netcode.gameobjects/Runtime/Configuration/CommandLineOptions.cs b/com.unity.netcode.gameobjects/Runtime/Configuration/CommandLineOptions.cs index c6f9b4a222..67878b125a 100644 --- a/com.unity.netcode.gameobjects/Runtime/Configuration/CommandLineOptions.cs +++ b/com.unity.netcode.gameobjects/Runtime/Configuration/CommandLineOptions.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using UnityEngine; namespace Unity.Netcode { @@ -13,6 +12,7 @@ public class CommandLineOptions /// /// Command-line options singleton /// + [Obsolete("Not used anymore replaced by TryGetArg")] public static CommandLineOptions Instance { get @@ -30,34 +30,41 @@ private set } private static CommandLineOptions s_Instance; - [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)] - private static void RuntimeInitializeOnLoad() => Instance = new CommandLineOptions(); - // Contains the current application instance domain's command line arguments - internal static List CommandLineArguments = new List(); - - // Invoked upon application start, after scene load - [RuntimeInitializeOnLoadMethod] - private static void ParseCommandLineArguments() - { - // Get all the command line arguments to be parsed later and/or modified - // prior to being parsed (for testing purposes). - CommandLineArguments = new List(Environment.GetCommandLineArgs()); - } + private static readonly List k_CommandLineArguments = new List(Environment.GetCommandLineArgs()); /// - /// Returns the value of an argument or null if there the argument is not present + /// Returns the value of an argument or null if the argument is not present /// /// The name of the argument /// Value of the command line argument passed in. + [Obsolete("Not used anymore replaced by TryGetArg")] public string GetArg(string arg) { - var argIndex = CommandLineArguments.IndexOf(arg); - if (argIndex >= 0 && argIndex < CommandLineArguments.Count - 1) + var argIndex = k_CommandLineArguments.IndexOf(arg); + if (argIndex >= 0 && argIndex < k_CommandLineArguments.Count - 1) { - return CommandLineArguments[argIndex + 1]; + return k_CommandLineArguments[argIndex + 1]; } return null; } + + /// + /// Returns true if the argument was found. + /// + /// The name of the argument to look up. + /// The argument's value, or if not found. + /// true if the argument was found; otherwise false. + public static bool TryGetArg(string arg, out string argValue) + { + var argIndex = k_CommandLineArguments.IndexOf(arg); + if (argIndex >= 0 && argIndex < k_CommandLineArguments.Count - 1) + { + argValue = k_CommandLineArguments[argIndex + 1]; + return true; + } + argValue = null; + return false; + } } } diff --git a/com.unity.netcode.gameobjects/Runtime/Core/ComponentFactory.cs b/com.unity.netcode.gameobjects/Runtime/Core/ComponentFactory.cs index 52945c4e46..6733429ee2 100644 --- a/com.unity.netcode.gameobjects/Runtime/Core/ComponentFactory.cs +++ b/com.unity.netcode.gameobjects/Runtime/Core/ComponentFactory.cs @@ -14,6 +14,10 @@ internal static class ComponentFactory internal delegate object CreateObjectDelegate(NetworkManager networkManager); private static Dictionary s_Delegates = new Dictionary(); +#if UNITY_EDITOR + [UnityEngine.RuntimeInitializeOnLoadMethod(UnityEngine.RuntimeInitializeLoadType.SubsystemRegistration)] + private static void ResetStaticsOnLoad() => s_Delegates = new Dictionary(); +#endif /// /// Instantiates an instance of a given interface diff --git a/com.unity.netcode.gameobjects/Runtime/Core/NetworkBehaviour.cs b/com.unity.netcode.gameobjects/Runtime/Core/NetworkBehaviour.cs index 59f9e637b6..28ec5033b8 100644 --- a/com.unity.netcode.gameobjects/Runtime/Core/NetworkBehaviour.cs +++ b/com.unity.netcode.gameobjects/Runtime/Core/NetworkBehaviour.cs @@ -1299,8 +1299,6 @@ internal void NetworkVariableUpdate(ulong targetClientId, bool forceSend = false } } - internal static bool LogSentVariableUpdateMessage; - private bool CouldHaveDirtyNetworkVariables() { // TODO: There should be a better way by reading one dirty variable vs. 'n' diff --git a/com.unity.netcode.gameobjects/Runtime/Core/NetworkManager.cs b/com.unity.netcode.gameobjects/Runtime/Core/NetworkManager.cs index 2e7e3a7572..4c7ca603db 100644 --- a/com.unity.netcode.gameobjects/Runtime/Core/NetworkManager.cs +++ b/com.unity.netcode.gameobjects/Runtime/Core/NetworkManager.cs @@ -21,6 +21,20 @@ namespace Unity.Netcode [HelpURL(HelpUrls.NetworkManager)] public class NetworkManager : MonoBehaviour, INetworkUpdateSystem { +#if UNITY_EDITOR + [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)] + private static void ResetStaticsOnLoad() + { + Singleton = null; + OnInstantiated = null; + OnDestroying = null; + OnSingletonReady = null; + OnNetworkManagerReset = null; + IsDistributedAuthority = false; + s_SerializedType = new List(); + DisableNotOptimizedSerializedType = false; + } +#endif /// /// Subscribe to this static event to get notifications when a instance has been instantiated. /// @@ -31,7 +45,6 @@ public class NetworkManager : MonoBehaviour, INetworkUpdateSystem /// public static event Action OnDestroying; - #if UNITY_EDITOR // Inspector view expand/collapse settings for this derived child class [HideInInspector] @@ -1800,6 +1813,10 @@ internal abstract class NetcodeAnalytics internal static ResetNetworkManagerDelegate OnNetworkManagerReset; + + /// + /// This is called by the Unity Editor reset button. See which is handled in "NetworkManagerHelper.cs". + /// private void Reset() { OnNetworkManagerReset?.Invoke(this); diff --git a/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs b/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs index 173d7fba8d..73cc6c7e2d 100644 --- a/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs +++ b/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs @@ -1741,6 +1741,8 @@ internal void SetIsDestroying() private void OnDestroy() { + SceneManager.activeSceneChanged -= CurrentlyActiveSceneChanged; + // Apply the is destroying flag SetIsDestroying(); @@ -2520,6 +2522,10 @@ private void OnTransformParentChanged() // If you couldn't find your parent, we put you into OrphanChildren set and every time we spawn another NetworkObject locally due to replication, // we call CheckOrphanChildren() method and quickly iterate over OrphanChildren set and see if we can reparent/adopt one. internal static HashSet OrphanChildren = new HashSet(); +#if UNITY_EDITOR + [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)] + private static void ResetStaticsOnLoad() => OrphanChildren = new HashSet(); +#endif internal bool ApplyNetworkParenting(bool removeParent = false, bool ignoreNotSpawned = false, bool orphanedChildPass = false, bool enableNotification = true) { diff --git a/com.unity.netcode.gameobjects/Runtime/Core/NetworkUpdateLoop.cs b/com.unity.netcode.gameobjects/Runtime/Core/NetworkUpdateLoop.cs index c48dea0333..5ac60e2cd4 100644 --- a/com.unity.netcode.gameobjects/Runtime/Core/NetworkUpdateLoop.cs +++ b/com.unity.netcode.gameobjects/Runtime/Core/NetworkUpdateLoop.cs @@ -70,19 +70,19 @@ public enum NetworkUpdateStage : byte ///
public static class NetworkUpdateLoop { - private static Dictionary> s_UpdateSystem_Sets; - private static Dictionary s_UpdateSystem_Arrays; - private const int k_UpdateSystem_InitialArrayCapacity = 1024; + private static Dictionary> s_UpdateSystemSets; + private static Dictionary s_UpdateSystemArrays; + private const int k_UpdateSystemInitialArrayCapacity = 1024; static NetworkUpdateLoop() { - s_UpdateSystem_Sets = new Dictionary>(); - s_UpdateSystem_Arrays = new Dictionary(); + s_UpdateSystemSets = new Dictionary>(); + s_UpdateSystemArrays = new Dictionary(); foreach (NetworkUpdateStage updateStage in Enum.GetValues(typeof(NetworkUpdateStage))) { - s_UpdateSystem_Sets.Add(updateStage, new HashSet()); - s_UpdateSystem_Arrays.Add(updateStage, new INetworkUpdateSystem[k_UpdateSystem_InitialArrayCapacity]); + s_UpdateSystemSets.Add(updateStage, new HashSet()); + s_UpdateSystemArrays.Add(updateStage, new INetworkUpdateSystem[k_UpdateSystemInitialArrayCapacity]); } } @@ -105,19 +105,19 @@ public static void RegisterAllNetworkUpdates(this INetworkUpdateSystem updateSys /// The being registered for the implementation public static void RegisterNetworkUpdate(this INetworkUpdateSystem updateSystem, NetworkUpdateStage updateStage = NetworkUpdateStage.Update) { - var sysSet = s_UpdateSystem_Sets[updateStage]; + var sysSet = s_UpdateSystemSets[updateStage]; if (!sysSet.Contains(updateSystem)) { sysSet.Add(updateSystem); int setLen = sysSet.Count; - var sysArr = s_UpdateSystem_Arrays[updateStage]; + var sysArr = s_UpdateSystemArrays[updateStage]; int arrLen = sysArr.Length; if (setLen > arrLen) { // double capacity - sysArr = s_UpdateSystem_Arrays[updateStage] = new INetworkUpdateSystem[arrLen *= 2]; + sysArr = s_UpdateSystemArrays[updateStage] = new INetworkUpdateSystem[arrLen *= 2]; } sysSet.CopyTo(sysArr); @@ -149,13 +149,13 @@ public static void UnregisterAllNetworkUpdates(this INetworkUpdateSystem updateS /// The to be deregistered from the implementation public static void UnregisterNetworkUpdate(this INetworkUpdateSystem updateSystem, NetworkUpdateStage updateStage = NetworkUpdateStage.Update) { - var sysSet = s_UpdateSystem_Sets[updateStage]; + var sysSet = s_UpdateSystemSets[updateStage]; if (sysSet.Contains(updateSystem)) { sysSet.Remove(updateSystem); int setLen = sysSet.Count; - var sysArr = s_UpdateSystem_Arrays[updateStage]; + var sysArr = s_UpdateSystemArrays[updateStage]; int arrLen = sysArr.Length; sysSet.CopyTo(sysArr); @@ -177,7 +177,7 @@ internal static void RunNetworkUpdateStage(NetworkUpdateStage updateStage) { UpdateStage = updateStage; - var sysArr = s_UpdateSystem_Arrays[updateStage]; + var sysArr = s_UpdateSystemArrays[updateStage]; int arrLen = sysArr.Length; for (int curIdx = 0; curIdx < arrLen; curIdx++) { @@ -291,6 +291,17 @@ public static PlayerLoopSystem CreateLoopSystem() [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)] private static void Initialize() { +#if UNITY_EDITOR + // Reset statics + s_UpdateSystemSets = new Dictionary>(); + s_UpdateSystemArrays = new Dictionary(); + foreach (NetworkUpdateStage updateStage in Enum.GetValues(typeof(NetworkUpdateStage))) + { + s_UpdateSystemSets.Add(updateStage, new HashSet()); + s_UpdateSystemArrays.Add(updateStage, new INetworkUpdateSystem[k_UpdateSystemInitialArrayCapacity]); + } + UpdateStage = default; +#endif UnregisterLoopSystems(); RegisterLoopSystems(); } diff --git a/com.unity.netcode.gameobjects/Runtime/Logging/NetworkLog.cs b/com.unity.netcode.gameobjects/Runtime/Logging/NetworkLog.cs index 29a092e77d..12fd9a60bb 100644 --- a/com.unity.netcode.gameobjects/Runtime/Logging/NetworkLog.cs +++ b/com.unity.netcode.gameobjects/Runtime/Logging/NetworkLog.cs @@ -6,13 +6,19 @@ namespace Unity.Netcode { + /// + /// Log configuration containing : + /// - used in LogContextNetworkManager.cs + /// - used in SceneEventData.cs + /// internal struct LogConfiguration { internal bool LogNetworkManagerRole; + internal bool LogSerializationOrder; } /// - /// Helper class for logging + /// Helper class for logging. /// public static class NetworkLog { @@ -58,7 +64,7 @@ internal static void ConfigureIntegrationTestLogging(NetworkManager networkManag internal static void LogWarning(Context context) => s_Log.Warning(context); /// - /// Locally logs a error log with Netcode prefixing. + /// Locally logs an error log with Netcode prefixing. /// /// The message to log [HideInCallstack] diff --git a/com.unity.netcode.gameobjects/Runtime/Messaging/DeferredMessageManager.cs b/com.unity.netcode.gameobjects/Runtime/Messaging/DeferredMessageManager.cs index f05d000fec..4d0cd8fec8 100644 --- a/com.unity.netcode.gameobjects/Runtime/Messaging/DeferredMessageManager.cs +++ b/com.unity.netcode.gameobjects/Runtime/Messaging/DeferredMessageManager.cs @@ -96,6 +96,10 @@ public virtual unsafe void CleanupStaleTriggers() /// Used for testing purposes ///
internal static bool IncludeMessageType = true; +#if UNITY_EDITOR + [UnityEngine.RuntimeInitializeOnLoadMethod(UnityEngine.RuntimeInitializeLoadType.SubsystemRegistration)] + private static void ResetStaticsOnLoad() => IncludeMessageType = true; +#endif private string GetWarningMessage(IDeferredNetworkMessageManager.TriggerType triggerType, ulong key, TriggerInfo triggerInfo, float spawnTimeout) { diff --git a/com.unity.netcode.gameobjects/Runtime/Messaging/ILPPMessageProvider.cs b/com.unity.netcode.gameobjects/Runtime/Messaging/ILPPMessageProvider.cs index ec460cc821..21413a4fae 100644 --- a/com.unity.netcode.gameobjects/Runtime/Messaging/ILPPMessageProvider.cs +++ b/com.unity.netcode.gameobjects/Runtime/Messaging/ILPPMessageProvider.cs @@ -53,6 +53,10 @@ internal struct ILPPMessageProvider : INetworkMessageProvider // Enable this for integration tests that need no message types defined internal static bool IntegrationTestNoMessages; +#if UNITY_EDITOR + [UnityEngine.RuntimeInitializeOnLoadMethod(UnityEngine.RuntimeInitializeLoadType.SubsystemRegistration)] + private static void ResetStaticsOnLoad() => IntegrationTestNoMessages = false; +#endif /// /// Returns a table of message type to NetworkMessageTypes enum value diff --git a/com.unity.netcode.gameobjects/Runtime/Messaging/INetworkMessage.cs b/com.unity.netcode.gameobjects/Runtime/Messaging/INetworkMessage.cs index fe481d25f5..7417bd6b85 100644 --- a/com.unity.netcode.gameobjects/Runtime/Messaging/INetworkMessage.cs +++ b/com.unity.netcode.gameobjects/Runtime/Messaging/INetworkMessage.cs @@ -1,4 +1,3 @@ - namespace Unity.Netcode { /// @@ -45,8 +44,10 @@ internal interface INetworkMessage public int Version { get; } } - - internal static class MessageDeliveryType where T : INetworkMessage +#if UNITY_6000_6_OR_NEWER + [Scripting.LifecycleManagement.AutoStaticsCleanup] +#endif + internal static partial class MessageDeliveryType where T : INetworkMessage { internal static NetworkDelivery DefaultDelivery { get; private set; } internal static void Initialize() diff --git a/com.unity.netcode.gameobjects/Runtime/Messaging/MessageDelivery.cs b/com.unity.netcode.gameobjects/Runtime/Messaging/MessageDelivery.cs index e8cf2ca6db..cc54d9d102 100644 --- a/com.unity.netcode.gameobjects/Runtime/Messaging/MessageDelivery.cs +++ b/com.unity.netcode.gameobjects/Runtime/Messaging/MessageDelivery.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using Unity.Netcode; -using UnityEditor; using UnityEngine; internal static class MessageDelivery @@ -15,17 +14,21 @@ internal static class MessageDelivery /// when sending the message via public API. /// - Skip the time sync messages since it has always used unreliable network delivery. /// - private static HashSet s_SkipMessageTypes = new HashSet(){ + private static readonly HashSet k_SkipMessageTypes = new HashSet(){ NetworkMessageTypes.NamedMessage, NetworkMessageTypes.Unnamed}; - [RuntimeInitializeOnLoadMethod] + [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)] private static void OnApplicationStart() { +#if UNITY_EDITOR + s_MessageToDelivery = new Dictionary(); + s_MessageToMessageType = new Dictionary(); +#endif UpdateMessageTypes(); } /// - /// FIrst pass at providing an easier path to configuring the network + /// First pass at providing an easier path to configuring the network /// delivery type for the message type. /// TODO: Once coalesces all reliable messages /// and/or organizes by a more unified order of operation tracking built into the @@ -40,7 +43,7 @@ private static void UpdateMessageTypes() foreach (var messageTypeObject in networkMessageTypes) { var messageType = (NetworkMessageTypes)messageTypeObject; - if (s_SkipMessageTypes.Contains(messageType)) + if (k_SkipMessageTypes.Contains(messageType)) { continue; } @@ -56,9 +59,13 @@ private static void UpdateMessageTypes() MessageDeliveryType.Initialize(); MessageDeliveryType.Initialize(); MessageDeliveryType.Initialize(); + MessageDeliveryType.Initialize(); + MessageDeliveryType.Initialize(); + MessageDeliveryType.Initialize(); MessageDeliveryType.Initialize(); MessageDeliveryType.Initialize(); MessageDeliveryType.Initialize(); + MessageDeliveryType.Initialize(); // RpcMessage.cs { MessageDeliveryType.Initialize(); @@ -71,18 +78,10 @@ private static void UpdateMessageTypes() MessageDeliveryType.Initialize(); } -#if UNITY_EDITOR - [InitializeOnLoadMethod] - [InitializeOnEnterPlayMode] - private static void OnEnterPlayMode() - { - UpdateMessageTypes(); - } -#endif internal static NetworkDelivery GetDelivery(Type type) { // Return the default if not registered or null - if (type == null || s_SkipMessageTypes.Contains(s_MessageToMessageType[type])) + if (type == null || k_SkipMessageTypes.Contains(s_MessageToMessageType[type])) { return NetworkDelivery.ReliableFragmentedSequenced; } @@ -91,7 +90,7 @@ internal static NetworkDelivery GetDelivery(Type type) internal static NetworkDelivery GetDelivery(NetworkMessageTypes messageType) { - if (s_SkipMessageTypes.Contains(messageType)) + if (k_SkipMessageTypes.Contains(messageType)) { throw new Exception($"{messageType} is not registered in the message type to network delivery map!"); } diff --git a/com.unity.netcode.gameobjects/Runtime/Messaging/NetworkMessageManager.cs b/com.unity.netcode.gameobjects/Runtime/Messaging/NetworkMessageManager.cs index c66aa62273..53714455c6 100644 --- a/com.unity.netcode.gameobjects/Runtime/Messaging/NetworkMessageManager.cs +++ b/com.unity.netcode.gameobjects/Runtime/Messaging/NetworkMessageManager.cs @@ -34,9 +34,9 @@ public InvalidMessageStructureException(string issue) : base(issue) internal class NetworkMessageManager : IDisposable { public bool StopProcessing = false; - private static Type s_ConnectionApprovedType = typeof(ConnectionApprovedMessage); - private static Type s_ConnectionRequestType = typeof(ConnectionRequestMessage); - private static Type s_DisconnectReasonType = typeof(DisconnectReasonMessage); + private static readonly Type k_ConnectionApprovedType = typeof(ConnectionApprovedMessage); + private static readonly Type k_ConnectionRequestType = typeof(ConnectionRequestMessage); + private static readonly Type k_DisconnectReasonType = typeof(DisconnectReasonMessage); private struct ReceiveQueueItem { @@ -149,8 +149,6 @@ public NetworkMessageManager(INetworkMessageSender sender, object owner, INetwor } } - internal static bool EnableMessageOrderConsoleLog = false; - public void Dispose() { if (m_Disposed) @@ -549,7 +547,7 @@ internal int GetMessageVersion(Type type, ulong clientId, bool forReceive = fals // Special cases because these are the messages that carry the version info - thus the version info isn't // populated yet when we get these. The first part of these messages always has to be the version data // and can't change. - if (messageType != s_ConnectionRequestType && messageType != s_ConnectionApprovedType && messageType != s_DisconnectReasonType && context.SenderId != manager.m_LocalClientId) + if (messageType != k_ConnectionRequestType && messageType != k_ConnectionApprovedType && messageType != k_DisconnectReasonType && context.SenderId != manager.m_LocalClientId) { messageVersion = manager.GetMessageVersion(messageType, context.SenderId, true); if (messageVersion < 0) @@ -603,7 +601,7 @@ internal int SendMessage(ref TMessageType messa var messageVersion = 0; // Special case because this is the message that carries the version info - thus the version info isn't populated yet when we get this. // The first part of this message always has to be the version data and can't change. - if (typeof(TMessageType) != s_ConnectionRequestType) + if (typeof(TMessageType) != k_ConnectionRequestType) { messageVersion = GetMessageVersion(typeof(TMessageType), clientIds[i]); if (messageVersion < 0) @@ -657,7 +655,7 @@ internal unsafe int SendPreSerializedMessage(in FastBufferWriter t // Special case because this is the message that carries the version info - thus the version info isn't populated yet when we get this. // The first part of this message always has to be the version data and can't change. - if (typeof(TMessageType) != s_ConnectionRequestType) + if (typeof(TMessageType) != k_ConnectionRequestType) { var messageVersion = GetMessageVersion(typeof(TMessageType), clientIds[i]); if (messageVersion < 0) @@ -741,7 +739,7 @@ internal unsafe int SendPreSerializedMessage(in FastBufferWriter t // Special case because this is the message that carries the version info - thus the version info isn't // populated yet when we get this. The first part of this message always has to be the version data // and can't change. - if (typeof(TMessageType) != s_ConnectionRequestType) + if (typeof(TMessageType) != k_ConnectionRequestType) { messageVersion = GetMessageVersion(typeof(TMessageType), clientId); if (messageVersion < 0) diff --git a/com.unity.netcode.gameobjects/Runtime/Metrics/NetworkMetrics.cs b/com.unity.netcode.gameobjects/Runtime/Metrics/NetworkMetrics.cs index b171932103..4bb7bb20a0 100644 --- a/com.unity.netcode.gameobjects/Runtime/Metrics/NetworkMetrics.cs +++ b/com.unity.netcode.gameobjects/Runtime/Metrics/NetworkMetrics.cs @@ -11,26 +11,22 @@ namespace Unity.Netcode internal class NetworkMetrics : INetworkMetrics { private const ulong k_MaxMetricsPerFrame = 1000L; - private static Dictionary s_SceneEventTypeNames; - private static ProfilerMarker s_FrameDispatch = new ProfilerMarker($"{nameof(NetworkMetrics)}.DispatchFrame"); + private static readonly Dictionary k_SceneEventTypeNames; + private static readonly ProfilerMarker k_FrameDispatch; static NetworkMetrics() { - s_SceneEventTypeNames = new Dictionary(); + k_SceneEventTypeNames = new Dictionary(); foreach (SceneEventType type in Enum.GetValues(typeof(SceneEventType))) { - s_SceneEventTypeNames[(uint)type] = type.ToString(); + k_SceneEventTypeNames[(uint)type] = type.ToString(); } + k_FrameDispatch = new ProfilerMarker($"{nameof(NetworkMetrics)}.DispatchFrame"); } private static string GetSceneEventTypeName(uint typeCode) { - if (!s_SceneEventTypeNames.TryGetValue(typeCode, out string name)) - { - name = "Unknown"; - } - - return name; + return k_SceneEventTypeNames.GetValueOrDefault(typeCode, "Unknown"); } private readonly Counter m_TransportBytesSent = new Counter(NetworkMetricTypes.TotalBytesSent.Id) @@ -511,9 +507,9 @@ public void UpdatePacketLoss(float packetLoss) public void DispatchFrame() { - s_FrameDispatch.Begin(); + k_FrameDispatch.Begin(); Dispatcher.Dispatch(); - s_FrameDispatch.End(); + k_FrameDispatch.End(); m_NumberOfMetricsThisFrame = 0; } diff --git a/com.unity.netcode.gameobjects/Runtime/NetworkVariable/Serialization/CollectionSerializationUtility.cs b/com.unity.netcode.gameobjects/Runtime/NetworkVariable/Serialization/CollectionSerializationUtility.cs index d9327f2441..7c1769d9d8 100644 --- a/com.unity.netcode.gameobjects/Runtime/NetworkVariable/Serialization/CollectionSerializationUtility.cs +++ b/com.unity.netcode.gameobjects/Runtime/NetworkVariable/Serialization/CollectionSerializationUtility.cs @@ -6,7 +6,7 @@ namespace Unity.Netcode { - internal static class CollectionSerializationUtility + internal static partial class CollectionSerializationUtility { public static void WriteNativeArrayDelta(FastBufferWriter writer, ref NativeArray value, ref NativeArray previousValue) where T : unmanaged { @@ -260,24 +260,24 @@ public static void ReadListDelta(FastBufferReader reader, ref List value) // so we're going to keep static lists that we can reuse in these methods. private static class ListCache { - private static List s_AddedList = new List(); - private static List s_RemovedList = new List(); - private static List s_ChangedList = new List(); + private static readonly List k_AddedList = new List(); + private static readonly List k_RemovedList = new List(); + private static readonly List k_ChangedList = new List(); public static List GetAddedList() { - s_AddedList.Clear(); - return s_AddedList; + k_AddedList.Clear(); + return k_AddedList; } public static List GetRemovedList() { - s_RemovedList.Clear(); - return s_RemovedList; + k_RemovedList.Clear(); + return k_RemovedList; } public static List GetChangedList() { - s_ChangedList.Clear(); - return s_ChangedList; + k_ChangedList.Clear(); + return k_ChangedList; } } diff --git a/com.unity.netcode.gameobjects/Runtime/NetworkVariable/Serialization/NetworkVariableSerialization.cs b/com.unity.netcode.gameobjects/Runtime/NetworkVariable/Serialization/NetworkVariableSerialization.cs index 1a00d2c86a..0c5528ca66 100644 --- a/com.unity.netcode.gameobjects/Runtime/NetworkVariable/Serialization/NetworkVariableSerialization.cs +++ b/com.unity.netcode.gameobjects/Runtime/NetworkVariable/Serialization/NetworkVariableSerialization.cs @@ -12,6 +12,8 @@ namespace Unity.Netcode [Serializable] public static class NetworkVariableSerialization { + // This is all setup in ILPP (in the file NetworkBehaviorILPP), using the functions in TypedILPPInitializers. + // There is no need to reset statics here. internal static INetworkVariableSerializer Serializer = new FallbackSerializer(); /// diff --git a/com.unity.netcode.gameobjects/Runtime/NetworkVariable/Serialization/UserNetworkVariableSerialization.cs b/com.unity.netcode.gameobjects/Runtime/NetworkVariable/Serialization/UserNetworkVariableSerialization.cs index 3f2a64585c..fe6a7acbba 100644 --- a/com.unity.netcode.gameobjects/Runtime/NetworkVariable/Serialization/UserNetworkVariableSerialization.cs +++ b/com.unity.netcode.gameobjects/Runtime/NetworkVariable/Serialization/UserNetworkVariableSerialization.cs @@ -7,7 +7,10 @@ namespace Unity.Netcode /// users to tell NetworkVariable about those extension methods (or simply pass in a lambda) /// /// The type of value being serialized - public class UserNetworkVariableSerialization +#if UNITY_6000_6_OR_NEWER + [Scripting.LifecycleManagement.AutoStaticsCleanup] +#endif + public partial class UserNetworkVariableSerialization { /// /// The write value delegate handler definition diff --git a/com.unity.netcode.gameobjects/Runtime/SceneManagement/DefaultSceneManagerHandler.cs b/com.unity.netcode.gameobjects/Runtime/SceneManagement/DefaultSceneManagerHandler.cs index 6e3954b294..76ea8feb04 100644 --- a/com.unity.netcode.gameobjects/Runtime/SceneManagement/DefaultSceneManagerHandler.cs +++ b/com.unity.netcode.gameobjects/Runtime/SceneManagement/DefaultSceneManagerHandler.cs @@ -206,7 +206,7 @@ public void PopulateLoadedScenes(ref Dictionary scene /// Unloads any scenes that have not been assigned. /// /// - public void UnloadUnassignedScenes(NetworkManager networkManager = null) + public void UnloadUnassignedScenes(NetworkManager networkManager) { var sceneManager = networkManager.SceneManager; SceneManager.sceneUnloaded += SceneManager_SceneUnloaded; diff --git a/com.unity.netcode.gameobjects/Runtime/SceneManagement/NetworkSceneManager.cs b/com.unity.netcode.gameobjects/Runtime/SceneManagement/NetworkSceneManager.cs index 78bdc7204a..8c07fbd603 100644 --- a/com.unity.netcode.gameobjects/Runtime/SceneManagement/NetworkSceneManager.cs +++ b/com.unity.netcode.gameobjects/Runtime/SceneManagement/NetworkSceneManager.cs @@ -5,6 +5,7 @@ using Unity.Collections; using UnityEngine; using UnityEngine.SceneManagement; +using Debug = UnityEngine.Debug; namespace Unity.Netcode @@ -548,6 +549,15 @@ internal bool RemoveServerClientSceneHandle(NetworkSceneHandle serverHandle, Net /// not destroy temporary scene are moved into the active scene /// internal static bool IsSpawnedObjectsPendingInDontDestroyOnLoad; +#if UNITY_EDITOR + [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)] + private static void ResetStaticsOnLoad() + { + DisableReSynchronization = false; + IsSpawnedObjectsPendingInDontDestroyOnLoad = false; + SceneUnloadEventHandler.ResetInstances(); + } +#endif /// /// Client and Server: @@ -590,7 +600,7 @@ internal bool HasSceneAuthority() } /// - /// Handle NetworkSeneManager clean up + /// Handle NetworkSceneManager clean up /// public void Dispose() { @@ -1570,6 +1580,9 @@ public SceneEventProgressStatus LoadScene(string sceneName, LoadSceneMode loadSc internal class SceneUnloadEventHandler { private static Dictionary> s_Instances = new Dictionary>(); +#if UNITY_EDITOR + internal static void ResetInstances() => s_Instances = new Dictionary>(); +#endif internal static void RegisterScene(NetworkSceneManager networkSceneManager, Scene scene, LoadSceneMode loadSceneMode, AsyncOperation asyncOperation = null) { diff --git a/com.unity.netcode.gameobjects/Runtime/SceneManagement/SceneEventData.cs b/com.unity.netcode.gameobjects/Runtime/SceneManagement/SceneEventData.cs index 0f41aad482..0ad4e96864 100644 --- a/com.unity.netcode.gameobjects/Runtime/SceneManagement/SceneEventData.cs +++ b/com.unity.netcode.gameobjects/Runtime/SceneManagement/SceneEventData.cs @@ -164,10 +164,10 @@ internal class SceneEventData : IDisposable /// we must distinguish which scene we are talking about when the server tells the client to unload a scene. /// The server will always communicate its local relative scene's handle and the client will determine its /// local relative handle from the table being built. - /// Look for usage to see where + /// Look for usage to see where /// entries are being added to or removed from the table /// - /// + /// /// internal void AddSceneToSynchronize(uint sceneHash, NetworkSceneHandle sceneHandle) { @@ -315,8 +315,6 @@ private void SortParentedNetworkObjects() } } - internal static bool LogSerializationOrder = false; - internal void AddSpawnedNetworkObjects() { m_NetworkObjectsSync.Clear(); @@ -352,7 +350,7 @@ private void SortObjectsToSync() // This is useful to know what NetworkObjects a client is going to be synchronized with // as well as the order in which they will be deserialized - if (LogSerializationOrder && m_NetworkManager.LogLevel == LogLevel.Developer) + if (NetworkLog.Config.LogSerializationOrder && m_NetworkManager.LogLevel == LogLevel.Developer) { var messageBuilder = new StringBuilder(0xFFFF); messageBuilder.AppendLine("[Server-Side Client-Synchronization] NetworkObject serialization order:"); @@ -1122,7 +1120,7 @@ internal void SynchronizeSceneNetworkObjects(NetworkManager networkManager) builder.AppendLine($"[Head: {noStart}][Tail: {noStop}][Size: {noStop - noStart}][{spawnedNetworkObject.name}][NID-{spawnedNetworkObject.NetworkObjectId}][Children: {spawnedNetworkObject.ChildNetworkBehaviours.Count}]"); LogArray(InternalBuffer.ToArray(), noStart, noStop, builder); } - // If we failed to deserialize the NetowrkObject then don't add null to the list + // If we failed to deserialize the NetworkObject then don't add null to the list if (spawnedNetworkObject != null) { if (!m_NetworkObjectsSync.Contains(spawnedNetworkObject)) @@ -1152,7 +1150,10 @@ internal void SynchronizeSceneNetworkObjects(NetworkManager networkManager) catch (Exception ex) { UnityEngine.Debug.LogException(ex); - UnityEngine.Debug.Log(builder.ToString()); + if (EnableSerializationLogs) + { + UnityEngine.Debug.Log(builder.ToString()); + } } finally { diff --git a/com.unity.netcode.gameobjects/Runtime/Serialization/FastBufferWriter.cs b/com.unity.netcode.gameobjects/Runtime/Serialization/FastBufferWriter.cs index bea537965e..ca39ea8c62 100644 --- a/com.unity.netcode.gameobjects/Runtime/Serialization/FastBufferWriter.cs +++ b/com.unity.netcode.gameobjects/Runtime/Serialization/FastBufferWriter.cs @@ -31,7 +31,7 @@ internal struct WriterHandle internal unsafe WriterHandle* Handle; - private static byte[] s_ByteArrayCache = new byte[65535]; + private static readonly byte[] k_ByteArrayCache = new byte[65535]; /// /// The current write position @@ -379,17 +379,17 @@ public unsafe byte[] ToArray() internal unsafe ArraySegment ToTempByteArray() { var length = Length; - if (length > s_ByteArrayCache.Length) + if (length > k_ByteArrayCache.Length) { return new ArraySegment(ToArray(), 0, length); } - fixed (byte* b = s_ByteArrayCache) + fixed (byte* b = k_ByteArrayCache) { UnsafeUtility.MemCpy(b, Handle->BufferPointer, length); } - return new ArraySegment(s_ByteArrayCache, 0, length); + return new ArraySegment(k_ByteArrayCache, 0, length); } /// diff --git a/com.unity.netcode.gameobjects/Runtime/Serialization/NetworkBehaviourReference.cs b/com.unity.netcode.gameobjects/Runtime/Serialization/NetworkBehaviourReference.cs index 19cecefc48..0b2e406e4f 100644 --- a/com.unity.netcode.gameobjects/Runtime/Serialization/NetworkBehaviourReference.cs +++ b/com.unity.netcode.gameobjects/Runtime/Serialization/NetworkBehaviourReference.cs @@ -11,8 +11,7 @@ public struct NetworkBehaviourReference : INetworkSerializable, IEquatable /// Creates a new instance of the struct. @@ -24,7 +23,7 @@ public NetworkBehaviourReference(NetworkBehaviour networkBehaviour) if (networkBehaviour == null) { m_NetworkObjectReference = new NetworkObjectReference((NetworkObject)null); - m_NetworkBehaviourId = s_NullId; + m_NetworkBehaviourId = k_NullId; return; } if (networkBehaviour.NetworkObject == null) @@ -64,7 +63,7 @@ public bool TryGet(out T networkBehaviour, NetworkManager networkManager = nu [MethodImpl(MethodImplOptions.AggressiveInlining)] private static NetworkBehaviour GetInternal(NetworkBehaviourReference networkBehaviourRef, NetworkManager networkManager = null) { - if (networkBehaviourRef.m_NetworkBehaviourId == s_NullId) + if (networkBehaviourRef.m_NetworkBehaviourId == k_NullId) { return null; } diff --git a/com.unity.netcode.gameobjects/Runtime/Serialization/NetworkObjectReference.cs b/com.unity.netcode.gameobjects/Runtime/Serialization/NetworkObjectReference.cs index 32ab0ed162..db5ed69ced 100644 --- a/com.unity.netcode.gameobjects/Runtime/Serialization/NetworkObjectReference.cs +++ b/com.unity.netcode.gameobjects/Runtime/Serialization/NetworkObjectReference.cs @@ -10,7 +10,7 @@ namespace Unity.Netcode public struct NetworkObjectReference : INetworkSerializable, IEquatable { private ulong m_NetworkObjectId; - private static ulong s_NullId = ulong.MaxValue; + private const ulong k_NullId = ulong.MaxValue; /// /// The of the referenced . @@ -31,7 +31,7 @@ public NetworkObjectReference(NetworkObject networkObject) { if (networkObject == null) { - m_NetworkObjectId = s_NullId; + m_NetworkObjectId = k_NullId; return; } @@ -53,7 +53,7 @@ public NetworkObjectReference(GameObject gameObject) { if (gameObject == null) { - m_NetworkObjectId = s_NullId; + m_NetworkObjectId = k_NullId; return; } @@ -92,7 +92,7 @@ public bool TryGet(out NetworkObject networkObject, NetworkManager networkManage [MethodImpl(MethodImplOptions.AggressiveInlining)] private static NetworkObject Resolve(NetworkObjectReference networkObjectRef, NetworkManager networkManager = null) { - if (networkObjectRef.m_NetworkObjectId == s_NullId) + if (networkObjectRef.m_NetworkObjectId == k_NullId) { return null; } diff --git a/com.unity.netcode.gameobjects/Runtime/Transports/SinglePlayer/SinglePlayerTransport.cs b/com.unity.netcode.gameobjects/Runtime/Transports/SinglePlayer/SinglePlayerTransport.cs index 7b2d6b1719..e94955618c 100644 --- a/com.unity.netcode.gameobjects/Runtime/Transports/SinglePlayer/SinglePlayerTransport.cs +++ b/com.unity.netcode.gameobjects/Runtime/Transports/SinglePlayer/SinglePlayerTransport.cs @@ -20,7 +20,7 @@ public class SinglePlayerTransport : NetworkTransport /// public override ulong ServerClientId { get; } = 0; - internal static string NotStartingAsHostErrorMessage = $"When using {nameof(SinglePlayerTransport)}, you must start a hosted session so both client and server are available locally."; + internal static readonly string NotStartingAsHostErrorMessage = $"When using {nameof(SinglePlayerTransport)}, you must start a hosted session so both client and server are available locally."; private struct MessageData { @@ -31,6 +31,10 @@ private struct MessageData } private static Dictionary> s_MessageQueue = new Dictionary>(); +#if UNITY_EDITOR + [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)] + private static void ResetStaticsOnLoad() => s_MessageQueue = new Dictionary>(); +#endif private ulong m_TransportId = 0; private NetworkManager m_NetworkManager; diff --git a/com.unity.netcode.gameobjects/Runtime/Transports/UTP/UnityTransport.cs b/com.unity.netcode.gameobjects/Runtime/Transports/UTP/UnityTransport.cs index e392af403c..fda0629b92 100644 --- a/com.unity.netcode.gameobjects/Runtime/Transports/UTP/UnityTransport.cs +++ b/com.unity.netcode.gameobjects/Runtime/Transports/UTP/UnityTransport.cs @@ -68,7 +68,7 @@ public enum ProtocolType // frame at 60 FPS. This will be a large over-estimation in any realistic scenario. private const int k_MaxReliableThroughput = (NetworkParameterConstants.MTU * 64 * 60) / 1000; // bytes per millisecond - private static ConnectionAddressData s_DefaultConnectionAddressData = new ConnectionAddressData { Address = "127.0.0.1", Port = 7777, WebSocketPath = "/", ServerListenAddress = string.Empty }; + private static readonly ConnectionAddressData k_DefaultConnectionAddressData = new ConnectionAddressData { Address = "127.0.0.1", Port = 7777, WebSocketPath = "/", ServerListenAddress = string.Empty }; #pragma warning disable IDE1006 // Naming Styles /// @@ -308,7 +308,7 @@ public NetworkEndpoint ListenEndPoint /// This is where you can change IP Address, Port, or server's listen address. /// /// - public ConnectionAddressData ConnectionData = s_DefaultConnectionAddressData; + public ConnectionAddressData ConnectionData = k_DefaultConnectionAddressData; /// /// Parameters for the Network Simulator @@ -369,6 +369,19 @@ private struct PacketLossCache #endif internal static event Action TransportInitialized; internal static event Action TransportDisposed; +#if UNITY_EDITOR + [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)] + private static void ResetStaticsOnLoad() + { + s_DriverConstructor = null; +#if UNITY_6000_2_OR_NEWER + OnDriverInitialized = null; + OnDisposingDriver = null; +#endif + TransportInitialized = null; + TransportDisposed = null; + } +#endif /// /// Provides access to the for this instance. @@ -856,7 +869,7 @@ private bool ParseCommandLineOptionsPort(out ushort port) return true; } #else - if (CommandLineOptions.Instance.GetArg(k_OverridePortArg) is string argValue) + if (CommandLineOptions.TryGetArg(k_OverridePortArg, out var argValue)) { port = (ushort)Convert.ChangeType(argValue, typeof(ushort)); return true; @@ -868,7 +881,7 @@ private bool ParseCommandLineOptionsPort(out ushort port) private bool ParseCommandLineOptionsAddress(out string ipValue) { - if (CommandLineOptions.Instance.GetArg(k_OverrideIpAddressArg) is string argValue) + if (CommandLineOptions.TryGetArg(k_OverrideIpAddressArg, out var argValue)) { ipValue = argValue; return true; diff --git a/com.unity.netcode.gameobjects/Tests/Runtime/TestHelpers/NetcodeIntegrationTest.cs b/com.unity.netcode.gameobjects/Tests/Runtime/TestHelpers/NetcodeIntegrationTest.cs index b9c5623759..f680b24338 100644 --- a/com.unity.netcode.gameobjects/Tests/Runtime/TestHelpers/NetcodeIntegrationTest.cs +++ b/com.unity.netcode.gameobjects/Tests/Runtime/TestHelpers/NetcodeIntegrationTest.cs @@ -2405,8 +2405,6 @@ public NetcodeIntegrationTest(HostOrServer hostOrServer) private void InitializeTestConfiguration(NetworkTopologyTypes networkTopologyType, HostOrServer? hostOrServer) { - NetworkMessageManager.EnableMessageOrderConsoleLog = false; - // Set m_NetworkTopologyType first because m_DistributedAuthority is calculated from it. m_NetworkTopologyType = networkTopologyType; diff --git a/testproject/ProjectSettings/EditorSettings.asset b/testproject/ProjectSettings/EditorSettings.asset index f92054474a..3ddd8f044f 100644 --- a/testproject/ProjectSettings/EditorSettings.asset +++ b/testproject/ProjectSettings/EditorSettings.asset @@ -23,7 +23,7 @@ EditorSettings: m_EnableTextureStreamingInEditMode: 1 m_EnableTextureStreamingInPlayMode: 1 m_AsyncShaderCompilation: 1 - m_EnterPlayModeOptionsEnabled: 0 + m_EnterPlayModeOptionsEnabled: 1 m_EnterPlayModeOptions: 3 m_ShowLightmapResolutionOverlay: 1 m_UseLegacyProbeSampleCount: 0 diff --git a/testproject/ProjectSettings/ProjectSettings.asset b/testproject/ProjectSettings/ProjectSettings.asset index 3bb16ba357..176a6e2232 100644 --- a/testproject/ProjectSettings/ProjectSettings.asset +++ b/testproject/ProjectSettings/ProjectSettings.asset @@ -741,8 +741,7 @@ PlayerSettings: scriptingDefineSymbols: Standalone: UNITY_NETCODE_NATIVE_COLLECTION_SUPPORT additionalCompilerArguments: - Standalone: - - -warnaserror + Standalone: [] platformArchitecture: {} scriptingBackend: {} il2cppCompilerConfiguration: {} From 7fd6ab9115da91275d94823fce25c4e397d7a836 Mon Sep 17 00:00:00 2001 From: mnachury-unity <120488254+mnachury-unity@users.noreply.github.com> Date: Tue, 2 Jun 2026 10:28:12 -0400 Subject: [PATCH 12/26] fix: end TransportConnect profiler marker on early returns in ConnectEventHandler (#4005) BeginSample was called at the top of ConnectEventHandler but two early return paths (duplicate server transport ID and duplicate client connect event) exited without calling EndSample, causing the 'Missing Profiler.EndSample' error surfaced by the MultipleConnectMessagesNoop playmode tests. --- .../Runtime/Connection/NetworkConnectionManager.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/com.unity.netcode.gameobjects/Runtime/Connection/NetworkConnectionManager.cs b/com.unity.netcode.gameobjects/Runtime/Connection/NetworkConnectionManager.cs index 1da9ddb3bb..abd63a8254 100644 --- a/com.unity.netcode.gameobjects/Runtime/Connection/NetworkConnectionManager.cs +++ b/com.unity.netcode.gameobjects/Runtime/Connection/NetworkConnectionManager.cs @@ -522,6 +522,9 @@ internal void ConnectEventHandler(ulong transportId) { NetworkLog.LogError($"[TransportApproval][Server] TransportId {transportId} is already connected to this server!"); } +#if DEVELOPMENT_BUILD || UNITY_EDITOR + s_TransportConnect.End(); +#endif return; } @@ -536,6 +539,9 @@ internal void ConnectEventHandler(ulong transportId) { NetworkLog.LogError("[TransportApproval][Client] Client received a transport connection event after already connecting!"); } +#if DEVELOPMENT_BUILD || UNITY_EDITOR + s_TransportConnect.End(); +#endif return; } From 8c78fc31cedbf69db527f286b3253b7a49d5d848 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Noellie=20V=C3=A9lez?= Date: Fri, 5 Jun 2026 15:55:02 +0200 Subject: [PATCH 13/26] refactor: NetworkAnimator cleanup and code quality improvements (#4001) refactor: cleanup and code quality improvements in NetworkAnimator - Add readonly to fields where applicable - Narrow internal fields and methods to private where applicable - Fix naming casing (TransitionStateinfo, m_DestinationStateToTransitioninfo, field prefixes) - Replace ContainsKey + Add/index lookups with TryAdd/TryGetValue - Remove unused variables and dead code - Improve XML summaries and comment clarity --------- Co-authored-by: Emma --- .../Runtime/Components/NetworkAnimator.cs | 175 ++++++++---------- 1 file changed, 73 insertions(+), 102 deletions(-) diff --git a/com.unity.netcode.gameobjects/Runtime/Components/NetworkAnimator.cs b/com.unity.netcode.gameobjects/Runtime/Components/NetworkAnimator.cs index 3fb8166408..246d6b8484 100644 --- a/com.unity.netcode.gameobjects/Runtime/Components/NetworkAnimator.cs +++ b/com.unity.netcode.gameobjects/Runtime/Components/NetworkAnimator.cs @@ -14,8 +14,8 @@ namespace Unity.Netcode.Components { internal class NetworkAnimatorStateChangeHandler : INetworkUpdateSystem { - private NetworkAnimator m_NetworkAnimator; - private bool m_IsServer; + private readonly NetworkAnimator m_NetworkAnimator; + private readonly bool m_IsServer; /// /// This removes sending RPCs from within RPCs when the @@ -132,14 +132,14 @@ private struct AnimationUpdate public NetworkAnimator.AnimationMessage AnimationMessage; } - private List m_SendAnimationUpdates = new List(); + private readonly List m_SendAnimationUpdates = new List(); /// /// Invoked when a server needs to forwarding an update to the animation state /// internal void SendAnimationUpdate(NetworkAnimator.AnimationMessage animationMessage, RpcParams rpcParams = default) { - m_SendAnimationUpdates.Add(new AnimationUpdate() { RpcParams = rpcParams, AnimationMessage = animationMessage }); + m_SendAnimationUpdates.Add(new AnimationUpdate { RpcParams = rpcParams, AnimationMessage = animationMessage }); } private struct ParameterUpdate @@ -148,17 +148,17 @@ private struct ParameterUpdate public NetworkAnimator.ParametersUpdateMessage ParametersUpdateMessage; } - private List m_SendParameterUpdates = new List(); + private readonly List m_SendParameterUpdates = new List(); /// /// Invoked when a server needs to forwarding an update to the parameter state /// internal void SendParameterUpdate(NetworkAnimator.ParametersUpdateMessage parametersUpdateMessage, RpcParams rpcParams = default) { - m_SendParameterUpdates.Add(new ParameterUpdate() { RpcParams = rpcParams, ParametersUpdateMessage = parametersUpdateMessage }); + m_SendParameterUpdates.Add(new ParameterUpdate { RpcParams = rpcParams, ParametersUpdateMessage = parametersUpdateMessage }); } - private List m_ProcessParameterUpdates = new List(); + private readonly List m_ProcessParameterUpdates = new List(); internal void ProcessParameterUpdate(NetworkAnimator.ParametersUpdateMessage parametersUpdateMessage) { m_ProcessParameterUpdates.Add(parametersUpdateMessage); @@ -171,31 +171,31 @@ private struct TriggerUpdate public NetworkAnimator.AnimationTriggerMessage AnimationTriggerMessage; } - private List m_SendTriggerUpdates = new List(); + private readonly List m_SendTriggerUpdates = new List(); /// /// Invoked when a server needs to forward an update to a Trigger state /// internal void QueueTriggerUpdateToClient(NetworkAnimator.AnimationTriggerMessage animationTriggerMessage, RpcParams clientRpcParams = default) { - m_SendTriggerUpdates.Add(new TriggerUpdate() { RpcParams = clientRpcParams, AnimationTriggerMessage = animationTriggerMessage }); + m_SendTriggerUpdates.Add(new TriggerUpdate { RpcParams = clientRpcParams, AnimationTriggerMessage = animationTriggerMessage }); } internal void QueueTriggerUpdateToServer(NetworkAnimator.AnimationTriggerMessage animationTriggerMessage) { - m_SendTriggerUpdates.Add(new TriggerUpdate() { AnimationTriggerMessage = animationTriggerMessage, SendToServer = true }); + m_SendTriggerUpdates.Add(new TriggerUpdate { AnimationTriggerMessage = animationTriggerMessage, SendToServer = true }); } internal void DeregisterUpdate() { - NetworkUpdateLoop.UnregisterNetworkUpdate(this, NetworkUpdateStage.PreUpdate); + this.UnregisterNetworkUpdate(NetworkUpdateStage.PreUpdate); } internal NetworkAnimatorStateChangeHandler(NetworkAnimator networkAnimator) { m_NetworkAnimator = networkAnimator; m_IsServer = networkAnimator.LocalNetworkManager.IsServer; - NetworkUpdateLoop.RegisterNetworkUpdate(this, NetworkUpdateStage.PreUpdate); + this.RegisterNetworkUpdate(NetworkUpdateStage.PreUpdate); } } @@ -213,7 +213,7 @@ public class NetworkAnimator : NetworkBehaviour, ISerializationCallbackReceiver #endif [Serializable] - internal class TransitionStateinfo + internal class TransitionStateInfo { public bool IsCrossFadeExit; public int Layer; @@ -260,11 +260,8 @@ public enum AuthorityModes /// public Animator Animator { - get { return m_Animator; } - set - { - m_Animator = value; - } + get => m_Animator; + set => m_Animator = value; } /// @@ -272,11 +269,11 @@ public Animator Animator /// [HideInInspector] [SerializeField] - internal List TransitionStateInfoList; + internal List TransitionStateInfoList; // Used to get the associated transition information required to synchronize late joining clients with transitions // [Layer][DestinationState][TransitionStateInfo] - private Dictionary> m_DestinationStateToTransitioninfo = new Dictionary>(); + private readonly Dictionary> m_DestinationStateToTransitionInfo = new Dictionary>(); // Named differently to avoid serialization conflicts with NetworkBehaviour internal NetworkManager LocalNetworkManager; @@ -284,21 +281,15 @@ public Animator Animator internal bool DistributedAuthorityMode; /// - /// Builds the m_DestinationStateToTransitioninfo lookup table + /// Builds the lookup table /// private void BuildDestinationToTransitionInfoTable() { foreach (var entry in TransitionStateInfoList) { - if (!m_DestinationStateToTransitioninfo.ContainsKey(entry.Layer)) - { - m_DestinationStateToTransitioninfo.Add(entry.Layer, new Dictionary()); - } - var destinationStateTransitionInfo = m_DestinationStateToTransitioninfo[entry.Layer]; - if (!destinationStateTransitionInfo.ContainsKey(entry.DestinationState)) - { - destinationStateTransitionInfo.Add(entry.DestinationState, entry); - } + m_DestinationStateToTransitionInfo.TryAdd(entry.Layer, new Dictionary()); + var destinationStateTransitionInfo = m_DestinationStateToTransitionInfo[entry.Layer]; + destinationStateTransitionInfo.TryAdd(entry.DestinationState, entry); } } @@ -323,14 +314,14 @@ internal class AnimatorParametersListContainer [SerializeField] internal AnimatorParametersListContainer AnimatorParameterEntries; - internal Dictionary AnimatorParameterEntryTable = new Dictionary(); + private readonly Dictionary m_AnimatorParameterEntryTable = new Dictionary(); #if UNITY_EDITOR [HideInInspector] [SerializeField] internal bool AnimatorParametersExpanded; - internal Dictionary ParameterToNameLookup = new Dictionary(); + private readonly Dictionary m_ParameterToNameLookup = new Dictionary(); private void ParseStateMachineStates(int layerIndex, ref AnimatorController animatorController, ref AnimatorStateMachine stateMachine) { @@ -371,7 +362,7 @@ private void ParseStateMachineStates(int layerIndex, ref AnimatorController anim } else if (transition.destinationState != null) { - var transitionInfo = new TransitionStateinfo() + var transitionInfo = new TransitionStateInfo() { Layer = layerIndex, OriginatingState = animatorState.nameHash, @@ -408,7 +399,7 @@ private void BuildTransitionStateInfoList() return; } - TransitionStateInfoList = new List(); + TransitionStateInfoList = new List(); var animControllerType = m_Animator.runtimeAnimatorController.GetType(); var animatorController = (AnimatorController)null; @@ -433,7 +424,7 @@ private void BuildTransitionStateInfoList() } } - internal void ProcessParameterEntries() + private void ProcessParameterEntries() { if (!Animator) { @@ -462,18 +453,18 @@ internal void ProcessParameterEntries() var parameters = animatorController.parameters; var parametersToRemove = new List(); - ParameterToNameLookup.Clear(); + m_ParameterToNameLookup.Clear(); foreach (var parameter in parameters) { - ParameterToNameLookup.Add(parameter.nameHash, parameter); + m_ParameterToNameLookup.Add(parameter.nameHash, parameter); } // Rebuild the parameter entry table for the inspector view - AnimatorParameterEntryTable.Clear(); + m_AnimatorParameterEntryTable.Clear(); foreach (var parameterEntry in AnimatorParameterEntries.ParameterEntries) { // Check for removed parameters. - if (!ParameterToNameLookup.ContainsKey(parameterEntry.NameHash)) + if (!m_ParameterToNameLookup.ContainsKey(parameterEntry.NameHash)) { parametersToRemove.Add(parameterEntry); // Skip this removed entry @@ -481,12 +472,9 @@ internal void ProcessParameterEntries() } // Build the list of known parameters - if (!AnimatorParameterEntryTable.ContainsKey(parameterEntry.NameHash)) - { - AnimatorParameterEntryTable.Add(parameterEntry.NameHash, parameterEntry); - } + m_AnimatorParameterEntryTable.TryAdd(parameterEntry.NameHash, parameterEntry); - var parameter = ParameterToNameLookup[parameterEntry.NameHash]; + var parameter = m_ParameterToNameLookup[parameterEntry.NameHash]; parameterEntry.name = parameter.name; parameterEntry.ParameterType = parameter.type; } @@ -498,9 +486,9 @@ internal void ProcessParameterEntries() } // Update any newly added parameters - foreach (var parameterLookUp in ParameterToNameLookup) + foreach (var parameterLookUp in m_ParameterToNameLookup) { - if (!AnimatorParameterEntryTable.ContainsKey(parameterLookUp.Value.nameHash)) + if (!m_AnimatorParameterEntryTable.ContainsKey(parameterLookUp.Value.nameHash)) { var animatorParameterEntry = new AnimatorParameterEntry() { @@ -510,7 +498,7 @@ internal void ProcessParameterEntries() Synchronize = true, }; AnimatorParameterEntries.ParameterEntries.Add(animatorParameterEntry); - AnimatorParameterEntryTable.Add(parameterLookUp.Value.nameHash, animatorParameterEntry); + m_AnimatorParameterEntryTable.Add(parameterLookUp.Value.nameHash, animatorParameterEntry); } } } @@ -602,7 +590,7 @@ public void NetworkSerialize(BufferSerializer serializer) where T : IReade serializer.SerializeValue(ref NormalizedTime); serializer.SerializeValue(ref Weight); - // Cross fading includes the duration of the cross fade. + // Cross-fading includes the duration of the cross-fade. if (CrossFade) { serializer.SerializeValue(ref Duration); @@ -680,7 +668,7 @@ public void NetworkSerialize(BufferSerializer serializer) where T : IReade /// Determines whether the is or based on the field. /// Optionally, you can still derive from and override the method. /// - /// or + /// True if this is server authoritative, false otherwise public bool IsServerAuthoritative() { return OnIsServerAuthoritative(); @@ -787,7 +775,7 @@ protected virtual void Awake() foreach (var parameterEntry in AnimatorParameterEntries.ParameterEntries) { - AnimatorParameterEntryTable.TryAdd(parameterEntry.NameHash, parameterEntry); + m_AnimatorParameterEntryTable.TryAdd(parameterEntry.NameHash, parameterEntry); } int layers = m_Animator.layerCount; @@ -813,10 +801,7 @@ protected virtual void Awake() // AnimationMessage. m_AnimationMessage.AnimationStates.Add(new AnimationState()); float layerWeightNow = m_Animator.GetLayerWeight(layer); - if (layerWeightNow != m_LayerWeights[layer]) - { - m_LayerWeights[layer] = layerWeightNow; - } + m_LayerWeights[layer] = layerWeightNow; } // The total initialization size calculated for the m_ParameterWriter write buffer. @@ -834,9 +819,9 @@ protected virtual void Awake() { var parameter = parameters[i]; var synchronizeParameter = true; - if (AnimatorParameterEntryTable.ContainsKey(parameter.nameHash)) + if (m_AnimatorParameterEntryTable.TryGetValue(parameter.nameHash, out var entry)) { - synchronizeParameter = AnimatorParameterEntryTable[parameter.nameHash].Synchronize; + synchronizeParameter = entry.Synchronize; } var cacheParam = new AnimatorParamCache @@ -862,8 +847,6 @@ protected virtual void Awake() var valueBool = m_Animator.GetBool(cacheParam.Hash); UnsafeUtility.WriteArrayElement(cacheParam.Value, 0, valueBool); break; - default: - break; } } @@ -985,12 +968,12 @@ private void WriteSynchronizationData(ref BufferSerializer serializer) whe var normalizedTime = synchronizationStateInfo.normalizedTime; var isInTransition = m_Animator.IsInTransition(layer); - // Grab one of the available AnimationState entries so we can fill it with the current + // Grab one of the available AnimationState entries, so we can fill it with the current // layer's animation state. var animationState = m_AnimationMessage.AnimationStates[layer]; // Synchronizing transitions with trigger conditions for late joining clients is now - // handled by cross fading between the late joining client's current layer's AnimationState + // handled by cross-fading between the late joining client's current layer's AnimationState // and the transition's destination AnimationState. if (isInTransition) { @@ -1012,16 +995,14 @@ private void WriteSynchronizationData(ref BufferSerializer serializer) whe } stateHash = nextState.fullPathHash; - // Use the destination state to transition info lookup table to see if this is a transition we can - // synchronize using cross fading - if (m_DestinationStateToTransitioninfo.ContainsKey(layer)) + // Check if this transition can be synchronized using cross-fading + if (m_DestinationStateToTransitionInfo.TryGetValue(layer, out var layerTransitions)) { - if (m_DestinationStateToTransitioninfo[layer].ContainsKey(nextState.shortNameHash)) + if (layerTransitions.TryGetValue(nextState.shortNameHash, out var transitionInfo)) { - var destinationInfo = m_DestinationStateToTransitioninfo[layer][nextState.shortNameHash]; - stateHash = destinationInfo.OriginatingState; - // Set the destination state to cross fade to from the originating state - animationState.DestinationStateHash = destinationInfo.DestinationState; + stateHash = transitionInfo.OriginatingState; + // Set the destination state to cross-fade to from the originating state + animationState.DestinationStateHash = transitionInfo.DestinationState; } } } @@ -1069,7 +1050,7 @@ protected override void OnSynchronize(ref BufferSerializer serializer) /// /// Checks for animation state changes in: /// -Layer weights - /// -Cross fades + /// -Cross-fades /// -Transitions /// -Layer AnimationStates /// @@ -1103,7 +1084,7 @@ private void CheckForStateChange(int layer) { m_TransitionHash[layer] = nt.fullPathHash; m_AnimationHash[layer] = 0; - // Next state is the destination state for cross fade + // Next state is the destination state for cross-fade animState.DestinationStateHash = nt.fullPathHash; animState.CrossFade = true; animState.Transition = true; @@ -1112,12 +1093,13 @@ private void CheckForStateChange(int layer) stateChangeDetected = true; //Debug.Log($"[Cross-Fade] To-Hash: {nt.fullPathHash} | TI-Duration: ({tt.duration}) | TI-Norm: ({tt.normalizedTime}) | From-Hash: ({m_AnimationHash[layer]}) | SI-FPHash: ({st.fullPathHash}) | SI-Norm: ({st.normalizedTime})"); } - // If we are not transitioned into the "any state" and the animator transition isn't a full path hash (layer to layer) and our pre-built destination state to transition does not contain the - // current layer (i.e. transitioning into a state from another layer) =or= we do contain the layer and the layer contains state to transition to is contained within our pre-built destination - // state then we can handle this transition as a non-cross fade state transition between layers. - // Otherwise, if we don't enter into this then this is a "trigger transition to some state that is now being transitioned back to the Idle state via trigger" or "Dual Triggers" IDLE<-->State. - else if (!tt.anyState && tt.fullPathHash != m_TransitionHash[layer] && (!m_DestinationStateToTransitioninfo.ContainsKey(layer) || - (m_DestinationStateToTransitioninfo.ContainsKey(layer) && m_DestinationStateToTransitioninfo[layer].ContainsKey(nt.fullPathHash)))) + // Handle as a non-cross-fade transition when: + // - not an "any state" transition and this is a new transition on this layer + // - the layer is either absent from the lookup table (cross-layer transition) or its destination state is present + // Skipping this block means we are in a "dual trigger" scenario where a trigger transitions + // to a state that is immediately transitioned back via another trigger (e.g. IDLE <--> State). + else if (!tt.anyState && tt.fullPathHash != m_TransitionHash[layer] && (!m_DestinationStateToTransitionInfo.TryGetValue(layer, out var layerTransitions) || + layerTransitions.ContainsKey(nt.fullPathHash))) { // first time in this transition for this layer m_TransitionHash[layer] = tt.fullPathHash; @@ -1127,7 +1109,7 @@ private void CheckForStateChange(int layer) animState.CrossFade = false; animState.Transition = true; animState.NormalizedTime = tt.normalizedTime; - if (m_DestinationStateToTransitioninfo.ContainsKey(layer) && m_DestinationStateToTransitioninfo[layer].ContainsKey(nt.fullPathHash)) + if (layerTransitions != null) { animState.DestinationStateHash = nt.fullPathHash; } @@ -1188,9 +1170,6 @@ internal void CheckForAnimatorChanges() // This sends updates only if a layer's state has changed for (int layer = 0; layer < m_Animator.layerCount; layer++) { - AnimatorStateInfo st = m_Animator.GetCurrentAnimatorStateInfo(layer); - var totalSpeed = st.speed * st.speedMultiplier; - var adjustedNormalizedMaxTime = totalSpeed > 0.0f ? 1.0f / totalSpeed : 0.0f; CheckForStateChange(layer); } @@ -1336,8 +1315,8 @@ private unsafe bool CheckParametersChanged() } /// - /// Writes all of the Animator's parameters - /// This uses the m_ParametersToUpdate list to write out only + /// Writes all the Animator's parameters + /// This uses the list to write out only /// the parameters that have changed /// private unsafe void WriteParameters(ref FastBufferWriter writer) @@ -1367,7 +1346,7 @@ private unsafe void WriteParameters(ref FastBufferWriter writer) BytePacker.WriteValuePacked(writer, (uint)valueInt); } } - else // Note: Triggers are treated like boolean values + else if (cacheValue.Type == AnimationParamEnumWrapper.AnimatorControllerParameterBool) { var valueBool = m_Animator.GetBool(hash); @@ -1452,7 +1431,7 @@ internal unsafe void UpdateParameters(ref ParametersUpdateMessage parametersUpda /// /// Applies the AnimationState state to the Animator /// - internal void UpdateAnimationState(AnimationState animationState) + private void UpdateAnimationState(AnimationState animationState) { // Handle updating layer weights first. if (animationState.Layer < m_LayerWeights.Length) @@ -1474,21 +1453,17 @@ internal void UpdateAnimationState(AnimationState animationState) // If it is a transition, then we are synchronizing transitions in progress when a client late joins if (animationState.Transition && !animationState.CrossFade) { - // We should have all valid entries for any animation state transition update - // Verify the AnimationState's assigned Layer exists - if (m_DestinationStateToTransitioninfo.ContainsKey(animationState.Layer)) + // At this point all entries in the lookup table should be valid. + // Look up the transition info for this layer and destination state. + if (m_DestinationStateToTransitionInfo.TryGetValue(animationState.Layer, out var layerTransitions)) { - // Verify the inner-table has the destination AnimationState name hash - if (m_DestinationStateToTransitioninfo[animationState.Layer].ContainsKey(animationState.DestinationStateHash)) + if (layerTransitions.TryGetValue(animationState.DestinationStateHash, out var transitionInfo)) { - // Make sure we are on the originating/starting state we are going to cross fade into + // Make sure we are on the originating/starting state we are going to cross-fade into if (currentState.shortNameHash == animationState.StateHash) { - // Get the transition state information - var transitionStateInfo = m_DestinationStateToTransitioninfo[animationState.Layer][animationState.DestinationStateHash]; - - // Cross fade from the current to the destination state for the transitions duration while starting at the server's current normalized time of the transition - m_Animator.CrossFade(transitionStateInfo.DestinationState, transitionStateInfo.TransitionDuration, transitionStateInfo.Layer, 0.0f, animationState.NormalizedTime); + // Cross-fade from the current to the destination state for the transitions duration while starting at the server's current normalized time of the transition + m_Animator.CrossFade(transitionInfo.DestinationState, transitionInfo.TransitionDuration, transitionInfo.Layer, 0.0f, animationState.NormalizedTime); } else if (LocalNetworkManager.LogLevel == LogLevel.Developer) { @@ -1525,7 +1500,7 @@ internal void UpdateAnimationState(AnimationState animationState) /// The server sets its local parameters and then forwards the message to the remaining clients /// [Rpc(SendTo.Server, AllowTargetOverride = true, InvokePermission = RpcInvokePermission.Owner)] - private unsafe void SendServerParametersUpdateRpc(ParametersUpdateMessage parametersUpdate, RpcParams rpcParams = default) + private void SendServerParametersUpdateRpc(ParametersUpdateMessage parametersUpdate, RpcParams rpcParams = default) { if (IsServerAuthoritative()) { @@ -1707,9 +1682,6 @@ internal void SendServerAnimTriggerRpc(AnimationTriggerMessage animationTriggerM } } - /// - /// See above - /// private void InternalSetTrigger(int hash, bool isSet = true) { m_Animator.SetBool(hash, isSet); @@ -1720,6 +1692,7 @@ private void InternalSetTrigger(int hash, bool isSet = true) /// to forward a trigger to a client /// /// the payload containing the trigger data to apply + /// Defined as it's used to send the RPC to be invoked on this client [Rpc(SendTo.NotAuthority, AllowTargetOverride = true, InvokePermission = RpcInvokePermission.Owner)] internal void SendAnimTriggerRpc(AnimationTriggerMessage animationTriggerMessage, RpcParams rpcParams = default) { @@ -1731,7 +1704,7 @@ internal void SendAnimTriggerRpc(AnimationTriggerMessage animationTriggerMessage /// a trigger to a client /// /// the payload containing the trigger data to apply - /// unused + /// used to send the RPC to be invoked on this client [Rpc(SendTo.NotServer, AllowTargetOverride = true)] internal void SendClientAnimTriggerRpc(AnimationTriggerMessage animationTriggerMessage, RpcParams rpcParams = default) { @@ -1774,13 +1747,11 @@ public void SetTrigger(int hash, bool setTrigger = true) { if (IsServer) { - /// as to why we queue m_NetworkAnimatorStateChangeHandler.QueueTriggerUpdateToClient(animTriggerMessage); InternalSetTrigger(hash, setTrigger); } else { - /// as to why we queue m_NetworkAnimatorStateChangeHandler.QueueTriggerUpdateToServer(animTriggerMessage); if (!IsServerAuthoritative()) { From 67e3e1ca3edfa42b8d59ebb85aff012bf89487f2 Mon Sep 17 00:00:00 2001 From: Noel Stephens Date: Tue, 9 Jun 2026 13:30:37 -0500 Subject: [PATCH 14/26] chore: replace DEVELOPMENT_BUILD || UNITY_EDITOR with DEBUG (#4006) * update Replacing DEVELOPMENT_BUILD || UNITY_EDITOR with DEBUG. Replacing niche DEVELOPMENT_BUILD uses with DEBUG. * update Removing extra files that didn't need to be included in this PR. * update Adding changelog entry * update Missed the codegen assembly. * update Upgrading our CI ubuntu image to: `package-ci/ubuntu-22.04:v4.86.0`. * Revert "update" This reverts commit 2b010d8e01d23acfe0ca18ef7f4f8a8f5c1e61ce. * update bumping ubuntu to use b1.large (segmentation fault was due to running out of memory). * Revert "update" This reverts commit bd1f00452e854b6be92d923eddb44189f1d2764e. * Reapply "update" This reverts commit 6a434bf2f6aa8b31b28e049eae662161102abf85. * update Switching PR triggers to use trunk as opposed to the pinned trunk. * Revert "update" This reverts commit 5ce1ef4245907ff7a6258f9f5f91e61a23439781. * update Moving pinned trunk up a bit. --- .yamato/project.metafile | 6 +-- com.unity.netcode.gameobjects/CHANGELOG.md | 1 + .../CodeGen/RuntimeAccessModifiersILPP.cs | 2 +- .../Connection/NetworkConnectionManager.cs | 22 ++++----- .../Runtime/Core/NetworkBehaviour.cs | 12 ++--- .../Runtime/Core/NetworkBehaviourUpdater.cs | 6 +-- .../Runtime/Core/NetworkManager.cs | 6 +-- .../Runtime/Messaging/CustomMessageManager.cs | 2 +- .../Runtime/Messaging/Messages/RpcMessages.cs | 2 +- .../Messaging/NetworkMessageManager.cs | 2 +- .../Messaging/RpcTargets/BaseRpcTarget.cs | 2 +- .../RpcTargets/LocalSendRpcTarget.cs | 2 +- .../RpcTargets/ProxyRpcTargetGroup.cs | 2 +- .../Messaging/RpcTargets/ServerRpcTarget.cs | 2 +- .../Runtime/Serialization/BitReader.cs | 12 ++--- .../Runtime/Serialization/BitWriter.cs | 12 ++--- .../Runtime/Serialization/FastBufferReader.cs | 34 ++++++------- .../Runtime/Serialization/FastBufferWriter.cs | 36 +++++++------- .../Runtime/Timing/NetworkTickSystem.cs | 6 +-- .../Runtime/Timing/NetworkTimeSystem.cs | 6 +-- .../Runtime/Transports/UTP/UnityTransport.cs | 2 +- .../SceneTransitioningBase1.unity | 48 ++++++++++++++++--- 22 files changed, 130 insertions(+), 95 deletions(-) diff --git a/.yamato/project.metafile b/.yamato/project.metafile index acc0c1d405..f4802d6ff0 100644 --- a/.yamato/project.metafile +++ b/.yamato/project.metafile @@ -47,7 +47,7 @@ test_platforms: type: Unity::VM image: package-ci/ubuntu-22.04:v4.85.0 flavor: b1.large - smaller_flavor: b1.medium + smaller_flavor: b1.large larger_flavor: b1.xlarge standalone: StandaloneLinux64 - name: win @@ -181,10 +181,10 @@ validation_editors: - 6000.4 - 6000.5 - trunk - - a4ce83e807ca9aff8394d1cc07341168dc49df03 + - f1298548e194f35ff7dfa6fee699d4464ab3919c minimal: - 6000.0 -pinnedTrunk: a4ce83e807ca9aff8394d1cc07341168dc49df03 +pinnedTrunk: f1298548e194f35ff7dfa6fee699d4464ab3919c # Scripting backends used by Standalone RunTimeTests--------------------------------------------------- diff --git a/com.unity.netcode.gameobjects/CHANGELOG.md b/com.unity.netcode.gameobjects/CHANGELOG.md index 0a4aa4fde3..3493b13666 100644 --- a/com.unity.netcode.gameobjects/CHANGELOG.md +++ b/com.unity.netcode.gameobjects/CHANGELOG.md @@ -14,6 +14,7 @@ Additional documentation and release notes are available at [Multiplayer Documen ### Changed +- Changed replaced define usages of `DEVELOPMENT_BUILD || UNITY_EDITOR` and a few niche uses of `DEVELOPMENT_BUILD` with `DEBUG`. (#4006) ### Deprecated diff --git a/com.unity.netcode.gameobjects/Editor/CodeGen/RuntimeAccessModifiersILPP.cs b/com.unity.netcode.gameobjects/Editor/CodeGen/RuntimeAccessModifiersILPP.cs index ffea9115b7..18c73ed737 100644 --- a/com.unity.netcode.gameobjects/Editor/CodeGen/RuntimeAccessModifiersILPP.cs +++ b/com.unity.netcode.gameobjects/Editor/CodeGen/RuntimeAccessModifiersILPP.cs @@ -159,7 +159,7 @@ private void ProcessNetworkBehaviour(TypeDefinition typeDefinition) { fieldDefinition.IsFamilyOrAssembly = true; } -#if MULTIPLAYER_TOOLS && (DEVELOPMENT_BUILD || UNITY_EDITOR || UNITY_MP_TOOLS_NET_STATS_MONITOR_ENABLED_IN_RELEASE) +#if MULTIPLAYER_TOOLS && (DEBUG || UNITY_MP_TOOLS_NET_STATS_MONITOR_ENABLED_IN_RELEASE) if (fieldDefinition.Name == nameof(NetworkBehaviour.__rpc_name_table)) { fieldDefinition.IsFamilyOrAssembly = true; diff --git a/com.unity.netcode.gameobjects/Runtime/Connection/NetworkConnectionManager.cs b/com.unity.netcode.gameobjects/Runtime/Connection/NetworkConnectionManager.cs index abd63a8254..5a8724374c 100644 --- a/com.unity.netcode.gameobjects/Runtime/Connection/NetworkConnectionManager.cs +++ b/com.unity.netcode.gameobjects/Runtime/Connection/NetworkConnectionManager.cs @@ -96,7 +96,7 @@ public struct ConnectionEventData /// public sealed class NetworkConnectionManager { -#if DEVELOPMENT_BUILD || UNITY_EDITOR +#if DEBUG private static ProfilerMarker s_TransportPollMarker = new ProfilerMarker($"{nameof(NetworkManager)}.TransportPoll"); private static ProfilerMarker s_TransportConnect = new ProfilerMarker($"{nameof(NetworkManager)}.TransportConnect"); private static ProfilerMarker s_HandleIncomingData = new ProfilerMarker($"{nameof(NetworkManager)}.{nameof(NetworkMessageManager.HandleIncomingData)}"); @@ -438,7 +438,7 @@ private ulong GetServerTransportId() internal void PollAndHandleNetworkEvents() { -#if DEVELOPMENT_BUILD || UNITY_EDITOR +#if DEBUG s_TransportPollMarker.Begin(); #endif NetworkEvent networkEvent; @@ -453,7 +453,7 @@ internal void PollAndHandleNetworkEvents() // Only do another iteration if: there are no more messages AND (there is no limit to max events or we have processed less than the maximum) } while (NetworkManager.IsListening && networkEvent != NetworkEvent.Nothing); -#if DEVELOPMENT_BUILD || UNITY_EDITOR +#if DEBUG s_TransportPollMarker.End(); #endif } @@ -501,7 +501,7 @@ internal void HandleNetworkEvent(NetworkEvent networkEvent, ulong transportClien /// internal void ConnectEventHandler(ulong transportId) { -#if DEVELOPMENT_BUILD || UNITY_EDITOR +#if DEBUG s_TransportConnect.Begin(); #endif // Assumptions: @@ -522,7 +522,7 @@ internal void ConnectEventHandler(ulong transportId) { NetworkLog.LogError($"[TransportApproval][Server] TransportId {transportId} is already connected to this server!"); } -#if DEVELOPMENT_BUILD || UNITY_EDITOR +#if DEBUG s_TransportConnect.End(); #endif return; @@ -539,7 +539,7 @@ internal void ConnectEventHandler(ulong transportId) { NetworkLog.LogError("[TransportApproval][Client] Client received a transport connection event after already connecting!"); } -#if DEVELOPMENT_BUILD || UNITY_EDITOR +#if DEBUG s_TransportConnect.End(); #endif return; @@ -577,7 +577,7 @@ internal void ConnectEventHandler(ulong transportId) StartClientApprovalCoroutine(clientId); } -#if DEVELOPMENT_BUILD || UNITY_EDITOR +#if DEBUG s_TransportConnect.End(); #endif } @@ -587,7 +587,7 @@ internal void ConnectEventHandler(ulong transportId) /// internal void DataEventHandler(ulong transportClientId, ref ArraySegment payload, float receiveTime) { -#if DEVELOPMENT_BUILD || UNITY_EDITOR +#if DEBUG s_HandleIncomingData.Begin(); #endif var (clientId, isConnectedClient) = TransportIdToClientId(transportClientId); @@ -596,7 +596,7 @@ internal void DataEventHandler(ulong transportClientId, ref ArraySegment p MessageManager.HandleIncomingData(clientId, payload, receiveTime); } -#if DEVELOPMENT_BUILD || UNITY_EDITOR +#if DEBUG s_HandleIncomingData.End(); #endif } @@ -639,7 +639,7 @@ internal void DisconnectEventHandler(ulong transportClientId) return; } -#if DEVELOPMENT_BUILD || UNITY_EDITOR +#if DEBUG s_TransportDisconnect.Begin(); #endif @@ -698,7 +698,7 @@ internal void DisconnectEventHandler(ulong transportClientId) NetworkManager.Shutdown(true); } } -#if DEVELOPMENT_BUILD || UNITY_EDITOR +#if DEBUG s_TransportDisconnect.End(); #endif } diff --git a/com.unity.netcode.gameobjects/Runtime/Core/NetworkBehaviour.cs b/com.unity.netcode.gameobjects/Runtime/Core/NetworkBehaviour.cs index 28ec5033b8..16b6d3be62 100644 --- a/com.unity.netcode.gameobjects/Runtime/Core/NetworkBehaviour.cs +++ b/com.unity.netcode.gameobjects/Runtime/Core/NetworkBehaviour.cs @@ -43,7 +43,7 @@ public abstract class NetworkBehaviour : MonoBehaviour internal static readonly Dictionary> __rpc_func_table = new Dictionary>(); internal static readonly Dictionary> __rpc_permission_table = new Dictionary>(); -#if MULTIPLAYER_TOOLS && (DEVELOPMENT_BUILD || UNITY_EDITOR || UNITY_MP_TOOLS_NET_STATS_MONITOR_ENABLED_IN_RELEASE) +#if MULTIPLAYER_TOOLS && (DEBUG || UNITY_MP_TOOLS_NET_STATS_MONITOR_ENABLED_IN_RELEASE) // RuntimeAccessModifiersILPP will make this `public` internal static readonly Dictionary> __rpc_name_table = new Dictionary>(); #endif @@ -145,7 +145,7 @@ internal void __endSendServerRpc(ref FastBufferWriter bufferWriter, uint rpcMeth bufferWriter.Dispose(); -#if MULTIPLAYER_TOOLS && (DEVELOPMENT_BUILD || UNITY_EDITOR || UNITY_MP_TOOLS_NET_STATS_MONITOR_ENABLED_IN_RELEASE) +#if MULTIPLAYER_TOOLS && (DEBUG || UNITY_MP_TOOLS_NET_STATS_MONITOR_ENABLED_IN_RELEASE) TrackRpcMetricsSend(ref serverRpcMessage, rpcMethodId, rpcWriteSize); #endif } @@ -269,7 +269,7 @@ internal void __endSendClientRpc(ref FastBufferWriter bufferWriter, uint rpcMeth } bufferWriter.Dispose(); -#if MULTIPLAYER_TOOLS && (DEVELOPMENT_BUILD || UNITY_EDITOR || UNITY_MP_TOOLS_NET_STATS_MONITOR_ENABLED_IN_RELEASE) +#if MULTIPLAYER_TOOLS && (DEBUG || UNITY_MP_TOOLS_NET_STATS_MONITOR_ENABLED_IN_RELEASE) if (!ValidateRpcMessageMetrics(GetType())) { return; @@ -1002,12 +1002,12 @@ internal void __registerRpc(uint hash, RpcReceiveHandler handler, string rpcMeth var rpcType = GetType(); __rpc_func_table[rpcType][hash] = handler; __rpc_permission_table[rpcType][hash] = permission; -#if MULTIPLAYER_TOOLS && (DEVELOPMENT_BUILD || UNITY_EDITOR || UNITY_MP_TOOLS_NET_STATS_MONITOR_ENABLED_IN_RELEASE) +#if MULTIPLAYER_TOOLS && (DEBUG || UNITY_MP_TOOLS_NET_STATS_MONITOR_ENABLED_IN_RELEASE) __rpc_name_table[rpcType][hash] = rpcMethodName; #endif } -#if MULTIPLAYER_TOOLS && (DEVELOPMENT_BUILD || UNITY_EDITOR || UNITY_MP_TOOLS_NET_STATS_MONITOR_ENABLED_IN_RELEASE) +#if MULTIPLAYER_TOOLS && (DEBUG || UNITY_MP_TOOLS_NET_STATS_MONITOR_ENABLED_IN_RELEASE) [MethodImpl(MethodImplOptions.AggressiveInlining)] private bool ValidateRpcMessageMetrics(Type type) { @@ -1125,7 +1125,7 @@ internal void InitializeVariables() { __rpc_func_table[GetType()] = new Dictionary(); __rpc_permission_table[GetType()] = new Dictionary(); -#if MULTIPLAYER_TOOLS && (DEVELOPMENT_BUILD || UNITY_EDITOR || UNITY_MP_TOOLS_NET_STATS_MONITOR_ENABLED_IN_RELEASE) +#if MULTIPLAYER_TOOLS && (DEBUG || UNITY_MP_TOOLS_NET_STATS_MONITOR_ENABLED_IN_RELEASE) __rpc_name_table[GetType()] = new Dictionary(); #endif __initializeRpcs(); diff --git a/com.unity.netcode.gameobjects/Runtime/Core/NetworkBehaviourUpdater.cs b/com.unity.netcode.gameobjects/Runtime/Core/NetworkBehaviourUpdater.cs index 4f12bca033..871c8afb7d 100644 --- a/com.unity.netcode.gameobjects/Runtime/Core/NetworkBehaviourUpdater.cs +++ b/com.unity.netcode.gameobjects/Runtime/Core/NetworkBehaviourUpdater.cs @@ -27,7 +27,7 @@ public class NetworkBehaviourUpdater /// private HashSet m_PendingDirtyNetworkObjects = new HashSet(); -#if DEVELOPMENT_BUILD || UNITY_EDITOR +#if DEBUG private ProfilerMarker m_NetworkBehaviourUpdate = new ProfilerMarker($"{nameof(NetworkBehaviour)}.{nameof(NetworkBehaviourUpdate)}"); #endif @@ -190,7 +190,7 @@ internal void ProcessDirtyObject(NetworkObject networkObject, bool forceSend) /// Refer to the definition. internal void NetworkBehaviourUpdate(bool forceSend = false) { -#if DEVELOPMENT_BUILD || UNITY_EDITOR +#if DEBUG m_NetworkBehaviourUpdate.Begin(); #endif try @@ -214,7 +214,7 @@ internal void NetworkBehaviourUpdate(bool forceSend = false) } finally { -#if DEVELOPMENT_BUILD || UNITY_EDITOR +#if DEBUG m_NetworkBehaviourUpdate.End(); #endif } diff --git a/com.unity.netcode.gameobjects/Runtime/Core/NetworkManager.cs b/com.unity.netcode.gameobjects/Runtime/Core/NetworkManager.cs index 4c7ca603db..453f10b88f 100644 --- a/com.unity.netcode.gameobjects/Runtime/Core/NetworkManager.cs +++ b/com.unity.netcode.gameobjects/Runtime/Core/NetworkManager.cs @@ -70,7 +70,7 @@ private static void ResetStaticsOnLoad() #pragma warning restore IDE1006 // restore naming rule violation check -#if DEVELOPMENT_BUILD || UNITY_EDITOR +#if DEBUG private static List s_SerializedType = new List(); // This is used to control the serialized type not optimized messaging for integration test purposes internal static bool DisableNotOptimizedSerializedType; @@ -1176,7 +1176,7 @@ public int MaximumFragmentedMessageSize internal void Initialize(bool server) { -#if DEVELOPMENT_BUILD || UNITY_EDITOR +#if DEBUG if (!DisableNotOptimizedSerializedType) { s_SerializedType.Clear(); @@ -1239,7 +1239,7 @@ internal void Initialize(bool server) MessageManager.Hook(new NetworkManagerHooks(this)); -#if DEVELOPMENT_BUILD || UNITY_EDITOR +#if DEBUG if (NetworkConfig.NetworkProfilingMetrics) { MessageManager.Hook(new ProfilingHooks()); diff --git a/com.unity.netcode.gameobjects/Runtime/Messaging/CustomMessageManager.cs b/com.unity.netcode.gameobjects/Runtime/Messaging/CustomMessageManager.cs index 15e30205f8..58a1f93470 100644 --- a/com.unity.netcode.gameobjects/Runtime/Messaging/CustomMessageManager.cs +++ b/com.unity.netcode.gameobjects/Runtime/Messaging/CustomMessageManager.cs @@ -434,7 +434,7 @@ public void SendNamedMessage(string messageName, IReadOnlyList clientIds, /// Exception thrown in case validation fails private unsafe void ValidateMessageSize(FastBufferWriter messageStream, NetworkDelivery networkDelivery, bool isNamed) { -#if DEVELOPMENT_BUILD || UNITY_EDITOR +#if DEBUG var maxNonFragmentedSize = m_NetworkManager.MessageManager.NonFragmentedMessageMaxSize - FastBufferWriter.GetWriteSize() - sizeof(NetworkBatchHeader); if (isNamed) { diff --git a/com.unity.netcode.gameobjects/Runtime/Messaging/Messages/RpcMessages.cs b/com.unity.netcode.gameobjects/Runtime/Messaging/Messages/RpcMessages.cs index 843d875216..b7f1320788 100644 --- a/com.unity.netcode.gameobjects/Runtime/Messaging/Messages/RpcMessages.cs +++ b/com.unity.netcode.gameobjects/Runtime/Messaging/Messages/RpcMessages.cs @@ -71,7 +71,7 @@ public static void Handle(ref NetworkContext context, ref RpcMetadata metadata, return; } -#if MULTIPLAYER_TOOLS && (DEVELOPMENT_BUILD || UNITY_EDITOR || UNITY_MP_TOOLS_NET_STATS_MONITOR_ENABLED_IN_RELEASE) +#if MULTIPLAYER_TOOLS && (DEBUG || UNITY_MP_TOOLS_NET_STATS_MONITOR_ENABLED_IN_RELEASE) networkBehaviour.TrackRpcMetricsReceive(ref metadata, ref context, payload.Length); #endif diff --git a/com.unity.netcode.gameobjects/Runtime/Messaging/NetworkMessageManager.cs b/com.unity.netcode.gameobjects/Runtime/Messaging/NetworkMessageManager.cs index 53714455c6..343cec9961 100644 --- a/com.unity.netcode.gameobjects/Runtime/Messaging/NetworkMessageManager.cs +++ b/com.unity.netcode.gameobjects/Runtime/Messaging/NetworkMessageManager.cs @@ -849,7 +849,7 @@ internal unsafe void ProcessSendQueues() } queueItem.Writer.Seek(0); -#if UNITY_EDITOR || DEVELOPMENT_BUILD +#if DEBUG // Skipping the Verify and sneaking the write mark in because we know it's fine. queueItem.Writer.Handle->AllowedWriteMark = sizeof(NetworkBatchHeader); #endif diff --git a/com.unity.netcode.gameobjects/Runtime/Messaging/RpcTargets/BaseRpcTarget.cs b/com.unity.netcode.gameobjects/Runtime/Messaging/RpcTargets/BaseRpcTarget.cs index a1e12a5310..a1b5b5d8aa 100644 --- a/com.unity.netcode.gameobjects/Runtime/Messaging/RpcTargets/BaseRpcTarget.cs +++ b/com.unity.netcode.gameobjects/Runtime/Messaging/RpcTargets/BaseRpcTarget.cs @@ -53,7 +53,7 @@ protected void CheckLockBeforeDispose() private protected void SendMessageToClient(NetworkBehaviour behaviour, ulong clientId, ref RpcMessage message, NetworkDelivery delivery) { var size = behaviour.NetworkManager.MessageManager.SendMessage(ref message, delivery, clientId); -#if MULTIPLAYER_TOOLS && (DEVELOPMENT_BUILD || UNITY_EDITOR || UNITY_MP_TOOLS_NET_STATS_MONITOR_ENABLED_IN_RELEASE) +#if MULTIPLAYER_TOOLS && (DEBUG || UNITY_MP_TOOLS_NET_STATS_MONITOR_ENABLED_IN_RELEASE) // Send to a specific client behaviour.TrackRpcMetricsSend(clientId, ref message, size); #endif diff --git a/com.unity.netcode.gameobjects/Runtime/Messaging/RpcTargets/LocalSendRpcTarget.cs b/com.unity.netcode.gameobjects/Runtime/Messaging/RpcTargets/LocalSendRpcTarget.cs index 386af8816f..4afa621d11 100644 --- a/com.unity.netcode.gameobjects/Runtime/Messaging/RpcTargets/LocalSendRpcTarget.cs +++ b/com.unity.netcode.gameobjects/Runtime/Messaging/RpcTargets/LocalSendRpcTarget.cs @@ -46,7 +46,7 @@ internal override void Send(NetworkBehaviour behaviour, ref RpcMessage message, message.Handle(ref context); length = tempBuffer.Length; } -#if MULTIPLAYER_TOOLS && (DEVELOPMENT_BUILD || UNITY_EDITOR || UNITY_MP_TOOLS_NET_STATS_MONITOR_ENABLED_IN_RELEASE) +#if MULTIPLAYER_TOOLS && (DEBUG || UNITY_MP_TOOLS_NET_STATS_MONITOR_ENABLED_IN_RELEASE) // Local invocation sends to self behaviour.TrackRpcMetricsSend(m_NetworkManager.LocalClientId, ref message, length); #endif diff --git a/com.unity.netcode.gameobjects/Runtime/Messaging/RpcTargets/ProxyRpcTargetGroup.cs b/com.unity.netcode.gameobjects/Runtime/Messaging/RpcTargets/ProxyRpcTargetGroup.cs index 8dd2e744eb..40321b3742 100644 --- a/com.unity.netcode.gameobjects/Runtime/Messaging/RpcTargets/ProxyRpcTargetGroup.cs +++ b/com.unity.netcode.gameobjects/Runtime/Messaging/RpcTargets/ProxyRpcTargetGroup.cs @@ -24,7 +24,7 @@ internal override void Send(NetworkBehaviour behaviour, ref RpcMessage message, } var proxyMessage = new ProxyMessage { Delivery = delivery, TargetClientIds = TargetClientIds.AsArray(), WrappedMessage = message }; var size = behaviour.NetworkManager.MessageManager.SendMessage(ref proxyMessage, delivery, NetworkManager.ServerClientId); -#if MULTIPLAYER_TOOLS && (DEVELOPMENT_BUILD || UNITY_EDITOR || UNITY_MP_TOOLS_NET_STATS_MONITOR_ENABLED_IN_RELEASE) +#if MULTIPLAYER_TOOLS && (DEBUG || UNITY_MP_TOOLS_NET_STATS_MONITOR_ENABLED_IN_RELEASE) foreach (var clientId in TargetClientIds) { behaviour.TrackRpcMetricsSend(clientId, ref message, size); diff --git a/com.unity.netcode.gameobjects/Runtime/Messaging/RpcTargets/ServerRpcTarget.cs b/com.unity.netcode.gameobjects/Runtime/Messaging/RpcTargets/ServerRpcTarget.cs index 63380ce40c..7382b41153 100644 --- a/com.unity.netcode.gameobjects/Runtime/Messaging/RpcTargets/ServerRpcTarget.cs +++ b/com.unity.netcode.gameobjects/Runtime/Messaging/RpcTargets/ServerRpcTarget.cs @@ -36,7 +36,7 @@ internal override void Send(NetworkBehaviour behaviour, ref RpcMessage message, using var tempBuffer = new FastBufferReader(message.WriteBuffer, Allocator.None); message.ReadBuffer = tempBuffer; message.Handle(ref context); -#if MULTIPLAYER_TOOLS && (DEVELOPMENT_BUILD || UNITY_EDITOR || UNITY_MP_TOOLS_NET_STATS_MONITOR_ENABLED_IN_RELEASE) +#if MULTIPLAYER_TOOLS && (DEBUG || UNITY_MP_TOOLS_NET_STATS_MONITOR_ENABLED_IN_RELEASE) // Local invocation sends to self behaviour.TrackRpcMetricsSend(m_NetworkManager.LocalClientId, ref message, tempBuffer.Length); #endif diff --git a/com.unity.netcode.gameobjects/Runtime/Serialization/BitReader.cs b/com.unity.netcode.gameobjects/Runtime/Serialization/BitReader.cs index dcb30a92dd..ab8c9bc5ce 100644 --- a/com.unity.netcode.gameobjects/Runtime/Serialization/BitReader.cs +++ b/com.unity.netcode.gameobjects/Runtime/Serialization/BitReader.cs @@ -15,7 +15,7 @@ public ref struct BitReader private readonly unsafe byte* m_BufferPointer; private readonly int m_Position; private int m_BitPosition; -#if DEVELOPMENT_BUILD || UNITY_EDITOR +#if DEBUG private int m_AllowedBitwiseReadMark; #endif @@ -39,7 +39,7 @@ internal unsafe BitReader(FastBufferReader reader) m_BufferPointer = m_Reader.Handle->BufferPointer + m_Reader.Handle->Position; m_Position = m_Reader.Handle->Position; m_BitPosition = 0; -#if DEVELOPMENT_BUILD || UNITY_EDITOR +#if DEBUG m_AllowedBitwiseReadMark = (m_Reader.Handle->AllowedReadMark - m_Position) * k_BitsPerByte; #endif } @@ -81,7 +81,7 @@ public unsafe bool TryBeginReadBits(uint bitCount) { return false; } -#if DEVELOPMENT_BUILD || UNITY_EDITOR +#if DEBUG m_AllowedBitwiseReadMark = (int)newBitPosition; #endif return true; @@ -94,7 +94,7 @@ public unsafe bool TryBeginReadBits(uint bitCount) /// Amount of bits to read public unsafe void ReadBits(out ulong value, uint bitCount) { -#if DEVELOPMENT_BUILD || UNITY_EDITOR +#if DEBUG if (bitCount > 64) { throw new ArgumentOutOfRangeException(nameof(bitCount), "Cannot read more than 64 bits from a 64-bit value!"); @@ -136,7 +136,7 @@ public unsafe void ReadBits(out ulong value, uint bitCount) /// Amount of bits to read. public void ReadBits(out byte value, uint bitCount) { -#if DEVELOPMENT_BUILD || UNITY_EDITOR +#if DEBUG int checkPos = (int)(m_BitPosition + bitCount); if (checkPos > m_AllowedBitwiseReadMark) { @@ -153,7 +153,7 @@ public void ReadBits(out byte value, uint bitCount) [MethodImpl(MethodImplOptions.AggressiveInlining)] public unsafe void ReadBit(out bool bit) { -#if DEVELOPMENT_BUILD || UNITY_EDITOR +#if DEBUG int checkPos = (m_BitPosition + 1); if (checkPos > m_AllowedBitwiseReadMark) { diff --git a/com.unity.netcode.gameobjects/Runtime/Serialization/BitWriter.cs b/com.unity.netcode.gameobjects/Runtime/Serialization/BitWriter.cs index 0e3ccfb7e3..7b38bcadc9 100644 --- a/com.unity.netcode.gameobjects/Runtime/Serialization/BitWriter.cs +++ b/com.unity.netcode.gameobjects/Runtime/Serialization/BitWriter.cs @@ -15,7 +15,7 @@ public ref struct BitWriter private unsafe byte* m_BufferPointer; private readonly int m_Position; private int m_BitPosition; -#if DEVELOPMENT_BUILD || UNITY_EDITOR +#if DEBUG private int m_AllowedBitwiseWriteMark; #endif private const int k_BitsPerByte = 8; @@ -37,7 +37,7 @@ internal unsafe BitWriter(FastBufferWriter writer) m_BufferPointer = writer.Handle->BufferPointer + writer.Handle->Position; m_Position = writer.Handle->Position; m_BitPosition = 0; -#if DEVELOPMENT_BUILD || UNITY_EDITOR +#if DEBUG m_AllowedBitwiseWriteMark = (m_Writer.Handle->AllowedWriteMark - m_Writer.Handle->Position) * k_BitsPerByte; #endif } @@ -97,7 +97,7 @@ public unsafe bool TryBeginWriteBits(int bitCount) return false; } } -#if DEVELOPMENT_BUILD || UNITY_EDITOR +#if DEBUG m_AllowedBitwiseWriteMark = newBitPosition; #endif return true; @@ -110,7 +110,7 @@ public unsafe bool TryBeginWriteBits(int bitCount) /// Amount of bits to write public unsafe void WriteBits(ulong value, uint bitCount) { -#if DEVELOPMENT_BUILD || UNITY_EDITOR +#if DEBUG if (bitCount > 64) { throw new ArgumentOutOfRangeException(nameof(bitCount), "Cannot write more than 64 bits from a 64-bit value!"); @@ -153,7 +153,7 @@ public unsafe void WriteBits(ulong value, uint bitCount) /// Amount of bits to write. public void WriteBits(byte value, uint bitCount) { -#if DEVELOPMENT_BUILD || UNITY_EDITOR +#if DEBUG int checkPos = (int)(m_BitPosition + bitCount); if (checkPos > m_AllowedBitwiseWriteMark) { @@ -174,7 +174,7 @@ public void WriteBits(byte value, uint bitCount) [MethodImpl(MethodImplOptions.AggressiveInlining)] public unsafe void WriteBit(bool bit) { -#if DEVELOPMENT_BUILD || UNITY_EDITOR +#if DEBUG int checkPos = (m_BitPosition + 1); if (checkPos > m_AllowedBitwiseWriteMark) { diff --git a/com.unity.netcode.gameobjects/Runtime/Serialization/FastBufferReader.cs b/com.unity.netcode.gameobjects/Runtime/Serialization/FastBufferReader.cs index d0dfdca7cd..d420ccd28e 100644 --- a/com.unity.netcode.gameobjects/Runtime/Serialization/FastBufferReader.cs +++ b/com.unity.netcode.gameobjects/Runtime/Serialization/FastBufferReader.cs @@ -20,7 +20,7 @@ internal struct ReaderHandle internal int Position; internal int Length; internal Allocator Allocator; -#if DEVELOPMENT_BUILD || UNITY_EDITOR +#if DEBUG internal int AllowedReadMark; internal bool InBitwiseContext; #endif @@ -55,7 +55,7 @@ public unsafe int Length internal unsafe void CommitBitwiseReads(int amount) { Handle->Position += amount; -#if DEVELOPMENT_BUILD || UNITY_EDITOR +#if DEBUG Handle->InBitwiseContext = false; #endif } @@ -83,7 +83,7 @@ internal unsafe void CommitBitwiseReads(int amount) // When we dispose, we are really only interested in disposing Allocator.Persistent and Allocator.TempJob // as disposing Allocator.Temp and Allocator.None would do nothing. Therefore, make sure we dispose the readerHandle with the right Allocator label readerHandle->Allocator = copyAllocator == Allocator.None ? internalAllocator : copyAllocator; -#if DEVELOPMENT_BUILD || UNITY_EDITOR +#if DEBUG readerHandle->AllowedReadMark = 0; readerHandle->InBitwiseContext = false; #endif @@ -321,7 +321,7 @@ public unsafe void Seek(int where) [MethodImpl(MethodImplOptions.AggressiveInlining)] internal unsafe void MarkBytesRead(int amount) { -#if DEVELOPMENT_BUILD || UNITY_EDITOR +#if DEBUG if (Handle->InBitwiseContext) { throw new InvalidOperationException( @@ -343,7 +343,7 @@ internal unsafe void MarkBytesRead(int amount) /// A BitReader public unsafe BitReader EnterBitwiseContext() { -#if DEVELOPMENT_BUILD || UNITY_EDITOR +#if DEBUG Handle->InBitwiseContext = true; #endif return new BitReader(this); @@ -366,7 +366,7 @@ public unsafe BitReader EnterBitwiseContext() [MethodImpl(MethodImplOptions.AggressiveInlining)] public unsafe bool TryBeginRead(int bytes) { -#if DEVELOPMENT_BUILD || UNITY_EDITOR +#if DEBUG if (Handle->InBitwiseContext) { throw new InvalidOperationException( @@ -377,7 +377,7 @@ public unsafe bool TryBeginRead(int bytes) { return false; } -#if DEVELOPMENT_BUILD || UNITY_EDITOR +#if DEBUG Handle->AllowedReadMark = Handle->Position + bytes; #endif return true; @@ -401,7 +401,7 @@ public unsafe bool TryBeginRead(int bytes) [MethodImpl(MethodImplOptions.AggressiveInlining)] public unsafe bool TryBeginReadValue(in T value) where T : unmanaged { -#if DEVELOPMENT_BUILD || UNITY_EDITOR +#if DEBUG if (Handle->InBitwiseContext) { throw new InvalidOperationException( @@ -413,7 +413,7 @@ public unsafe bool TryBeginReadValue(in T value) where T : unmanaged { return false; } -#if DEVELOPMENT_BUILD || UNITY_EDITOR +#if DEBUG Handle->AllowedReadMark = Handle->Position + len; #endif return true; @@ -429,7 +429,7 @@ public unsafe bool TryBeginReadValue(in T value) where T : unmanaged [MethodImpl(MethodImplOptions.AggressiveInlining)] internal unsafe bool TryBeginReadInternal(int bytes) { -#if DEVELOPMENT_BUILD || UNITY_EDITOR +#if DEBUG if (Handle->InBitwiseContext) { throw new InvalidOperationException( @@ -440,7 +440,7 @@ internal unsafe bool TryBeginReadInternal(int bytes) { return false; } -#if DEVELOPMENT_BUILD || UNITY_EDITOR +#if DEBUG if (Handle->Position + bytes > Handle->AllowedReadMark) { Handle->AllowedReadMark = Handle->Position + bytes; @@ -602,7 +602,7 @@ public unsafe void ReadValue(out string s, bool oneByteChars = false) /// Whether or not to use one byte per character. This will only allow ASCII public unsafe void ReadValueSafe(out string s, bool oneByteChars = false) { -#if DEVELOPMENT_BUILD || UNITY_EDITOR +#if DEBUG if (Handle->InBitwiseContext) { throw new InvalidOperationException( @@ -679,7 +679,7 @@ private void ReadLength(out int length) [MethodImpl(MethodImplOptions.AggressiveInlining)] public unsafe void ReadPartialValue(out T value, int bytesToRead, int offsetBytes = 0) where T : unmanaged { -#if DEVELOPMENT_BUILD || UNITY_EDITOR +#if DEBUG if (Handle->InBitwiseContext) { throw new InvalidOperationException( @@ -707,7 +707,7 @@ public unsafe void ReadPartialValue(out T value, int bytesToRead, int offsetB [MethodImpl(MethodImplOptions.AggressiveInlining)] public unsafe void ReadByte(out byte value) { -#if DEVELOPMENT_BUILD || UNITY_EDITOR +#if DEBUG if (Handle->InBitwiseContext) { throw new InvalidOperationException( @@ -731,7 +731,7 @@ public unsafe void ReadByte(out byte value) [MethodImpl(MethodImplOptions.AggressiveInlining)] public unsafe void ReadByteSafe(out byte value) { -#if DEVELOPMENT_BUILD || UNITY_EDITOR +#if DEBUG if (Handle->InBitwiseContext) { throw new InvalidOperationException( @@ -755,7 +755,7 @@ public unsafe void ReadByteSafe(out byte value) [MethodImpl(MethodImplOptions.AggressiveInlining)] public unsafe void ReadBytes(byte* value, int size, int offset = 0) { -#if DEVELOPMENT_BUILD || UNITY_EDITOR +#if DEBUG if (Handle->InBitwiseContext) { throw new InvalidOperationException( @@ -782,7 +782,7 @@ public unsafe void ReadBytes(byte* value, int size, int offset = 0) [MethodImpl(MethodImplOptions.AggressiveInlining)] public unsafe void ReadBytesSafe(byte* value, int size, int offset = 0) { -#if DEVELOPMENT_BUILD || UNITY_EDITOR +#if DEBUG if (Handle->InBitwiseContext) { throw new InvalidOperationException( diff --git a/com.unity.netcode.gameobjects/Runtime/Serialization/FastBufferWriter.cs b/com.unity.netcode.gameobjects/Runtime/Serialization/FastBufferWriter.cs index ca39ea8c62..343822fe32 100644 --- a/com.unity.netcode.gameobjects/Runtime/Serialization/FastBufferWriter.cs +++ b/com.unity.netcode.gameobjects/Runtime/Serialization/FastBufferWriter.cs @@ -23,7 +23,7 @@ internal struct WriterHandle internal int MaxCapacity; internal Allocator Allocator; internal bool BufferGrew; -#if DEVELOPMENT_BUILD || UNITY_EDITOR +#if DEBUG internal int AllowedWriteMark; internal bool InBitwiseContext; #endif @@ -79,7 +79,7 @@ public unsafe int Length internal unsafe void CommitBitwiseWrites(int amount) { Handle->Position += amount; -#if DEVELOPMENT_BUILD || UNITY_EDITOR +#if DEBUG Handle->InBitwiseContext = false; #endif } @@ -97,7 +97,7 @@ public unsafe FastBufferWriter(int size, Allocator allocator, int maxSize = -1) // If the buffer grows, a new buffer will be allocated and the handle pointer pointed at the new location... // The original buffer won't be deallocated until the writer is destroyed since it's part of the handle allocation. Handle = (WriterHandle*)UnsafeUtility.Malloc(sizeof(WriterHandle) + size, UnsafeUtility.AlignOf(), allocator); -#if DEVELOPMENT_BUILD || UNITY_EDITOR +#if DEBUG UnsafeUtility.MemSet(Handle, 0, sizeof(WriterHandle) + size); #endif Handle->BufferPointer = (byte*)(Handle + 1); @@ -107,7 +107,7 @@ public unsafe FastBufferWriter(int size, Allocator allocator, int maxSize = -1) Handle->Allocator = allocator; Handle->MaxCapacity = maxSize < size ? size : maxSize; Handle->BufferGrew = false; -#if DEVELOPMENT_BUILD || UNITY_EDITOR +#if DEBUG Handle->AllowedWriteMark = 0; Handle->InBitwiseContext = false; #endif @@ -185,7 +185,7 @@ public unsafe void Truncate(int where = -1) /// A BitWriter public unsafe BitWriter EnterBitwiseContext() { -#if DEVELOPMENT_BUILD || UNITY_EDITOR +#if DEBUG Handle->InBitwiseContext = true; #endif return new BitWriter(this); @@ -201,7 +201,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 +#if DEBUG UnsafeUtility.MemSet(newBuffer, 0, newSize); #endif UnsafeUtility.MemCpy(newBuffer, Handle->BufferPointer, Length); @@ -232,7 +232,7 @@ internal unsafe void Grow(int additionalSizeRequired) [MethodImpl(MethodImplOptions.AggressiveInlining)] public unsafe bool TryBeginWrite(int bytes) { -#if DEVELOPMENT_BUILD || UNITY_EDITOR +#if DEBUG if (Handle->InBitwiseContext) { throw new InvalidOperationException( @@ -255,7 +255,7 @@ public unsafe bool TryBeginWrite(int bytes) return false; } } -#if DEVELOPMENT_BUILD || UNITY_EDITOR +#if DEBUG Handle->AllowedWriteMark = Handle->Position + bytes; #endif return true; @@ -280,7 +280,7 @@ public unsafe bool TryBeginWrite(int bytes) [MethodImpl(MethodImplOptions.AggressiveInlining)] public unsafe bool TryBeginWriteValue(in T value) where T : unmanaged { -#if DEVELOPMENT_BUILD || UNITY_EDITOR +#if DEBUG if (Handle->InBitwiseContext) { throw new InvalidOperationException( @@ -304,7 +304,7 @@ public unsafe bool TryBeginWriteValue(in T value) where T : unmanaged return false; } } -#if DEVELOPMENT_BUILD || UNITY_EDITOR +#if DEBUG Handle->AllowedWriteMark = Handle->Position + len; #endif return true; @@ -320,7 +320,7 @@ public unsafe bool TryBeginWriteValue(in T value) where T : unmanaged [MethodImpl(MethodImplOptions.AggressiveInlining)] public unsafe bool TryBeginWriteInternal(int bytes) { -#if DEVELOPMENT_BUILD || UNITY_EDITOR +#if DEBUG if (Handle->InBitwiseContext) { throw new InvalidOperationException( @@ -343,7 +343,7 @@ public unsafe bool TryBeginWriteInternal(int bytes) return false; } } -#if DEVELOPMENT_BUILD || UNITY_EDITOR +#if DEBUG if (Handle->Position + bytes > Handle->AllowedWriteMark) { Handle->AllowedWriteMark = Handle->Position + bytes; @@ -523,7 +523,7 @@ public unsafe void WriteValue(string s, bool oneByteChars = false) /// Whether or not to use one byte per character. This will only allow ASCII public unsafe void WriteValueSafe(string s, bool oneByteChars = false) { -#if DEVELOPMENT_BUILD || UNITY_EDITOR +#if DEBUG if (Handle->InBitwiseContext) { throw new InvalidOperationException( @@ -618,7 +618,7 @@ public static unsafe int GetWriteSize(NativeList array, int count = -1, in [MethodImpl(MethodImplOptions.AggressiveInlining)] public unsafe void WritePartialValue(T value, int bytesToWrite, int offsetBytes = 0) where T : unmanaged { -#if DEVELOPMENT_BUILD || UNITY_EDITOR +#if DEBUG if (Handle->InBitwiseContext) { throw new InvalidOperationException( @@ -644,7 +644,7 @@ public unsafe void WritePartialValue(T value, int bytesToWrite, int offsetByt [MethodImpl(MethodImplOptions.AggressiveInlining)] public unsafe void WriteByte(byte value) { -#if DEVELOPMENT_BUILD || UNITY_EDITOR +#if DEBUG if (Handle->InBitwiseContext) { throw new InvalidOperationException( @@ -668,7 +668,7 @@ public unsafe void WriteByte(byte value) [MethodImpl(MethodImplOptions.AggressiveInlining)] public unsafe void WriteByteSafe(byte value) { -#if DEVELOPMENT_BUILD || UNITY_EDITOR +#if DEBUG if (Handle->InBitwiseContext) { throw new InvalidOperationException( @@ -692,7 +692,7 @@ public unsafe void WriteByteSafe(byte value) [MethodImpl(MethodImplOptions.AggressiveInlining)] public unsafe void WriteBytes(byte* value, int size, int offset = 0) { -#if DEVELOPMENT_BUILD || UNITY_EDITOR +#if DEBUG if (Handle->InBitwiseContext) { throw new InvalidOperationException( @@ -719,7 +719,7 @@ public unsafe void WriteBytes(byte* value, int size, int offset = 0) [MethodImpl(MethodImplOptions.AggressiveInlining)] public unsafe void WriteBytesSafe(byte* value, int size, int offset = 0) { -#if DEVELOPMENT_BUILD || UNITY_EDITOR +#if DEBUG if (Handle->InBitwiseContext) { throw new InvalidOperationException( diff --git a/com.unity.netcode.gameobjects/Runtime/Timing/NetworkTickSystem.cs b/com.unity.netcode.gameobjects/Runtime/Timing/NetworkTickSystem.cs index be03c1b478..821f727ed3 100644 --- a/com.unity.netcode.gameobjects/Runtime/Timing/NetworkTickSystem.cs +++ b/com.unity.netcode.gameobjects/Runtime/Timing/NetworkTickSystem.cs @@ -10,7 +10,7 @@ namespace Unity.Netcode [Serializable] public class NetworkTickSystem { -#if DEVELOPMENT_BUILD || UNITY_EDITOR +#if DEBUG private static ProfilerMarker s_Tick = new ProfilerMarker($"{nameof(NetworkTickSystem)}.Tick"); #endif @@ -97,11 +97,11 @@ public void UpdateTick(double localTimeSec, double serverTimeSec) LocalTime = new NetworkTime(TickRate, i); ServerTime = new NetworkTime(TickRate, i - localToServerDifference); -#if DEVELOPMENT_BUILD || UNITY_EDITOR +#if DEBUG s_Tick.Begin(); #endif Tick?.Invoke(); -#if DEVELOPMENT_BUILD || UNITY_EDITOR +#if DEBUG s_Tick.End(); #endif } diff --git a/com.unity.netcode.gameobjects/Runtime/Timing/NetworkTimeSystem.cs b/com.unity.netcode.gameobjects/Runtime/Timing/NetworkTimeSystem.cs index 16f4173f24..a967052a16 100644 --- a/com.unity.netcode.gameobjects/Runtime/Timing/NetworkTimeSystem.cs +++ b/com.unity.netcode.gameobjects/Runtime/Timing/NetworkTimeSystem.cs @@ -34,7 +34,7 @@ public class NetworkTimeSystem /// private const double k_DefaultAdjustmentRatio = 0.01d; -#if DEVELOPMENT_BUILD || UNITY_EDITOR +#if DEBUG private static ProfilerMarker s_SyncTime = new ProfilerMarker($"{nameof(NetworkManager)}.SyncTime"); #endif private double m_PreviousTimeSec; @@ -182,7 +182,7 @@ internal void UpdateTime() /// private void OnTickSyncTime() { -#if DEVELOPMENT_BUILD || UNITY_EDITOR +#if DEBUG s_SyncTime.Begin(); #endif @@ -196,7 +196,7 @@ private void OnTickSyncTime() m_ConnectionManager.SendMessage(ref message, m_NetworkDelivery, m_ConnectionManager.ConnectedClientIds); } -#if DEVELOPMENT_BUILD || UNITY_EDITOR +#if DEBUG s_SyncTime.End(); #endif } diff --git a/com.unity.netcode.gameobjects/Runtime/Transports/UTP/UnityTransport.cs b/com.unity.netcode.gameobjects/Runtime/Transports/UTP/UnityTransport.cs index fda0629b92..cac615f394 100644 --- a/com.unity.netcode.gameobjects/Runtime/Transports/UTP/UnityTransport.cs +++ b/com.unity.netcode.gameobjects/Runtime/Transports/UTP/UnityTransport.cs @@ -1,7 +1,7 @@ // NetSim Implementation compilation boilerplate // All references to UNITY_MP_TOOLS_NETSIM_IMPLEMENTATION_ENABLED should be defined in the same way, // as any discrepancies are likely to result in build failures -#if UNITY_EDITOR || (DEVELOPMENT_BUILD && !UNITY_MP_TOOLS_NETSIM_DISABLED_IN_DEVELOP) || (!DEVELOPMENT_BUILD && UNITY_MP_TOOLS_NETSIM_ENABLED_IN_RELEASE) +#if UNITY_EDITOR || (DEBUG && !UNITY_MP_TOOLS_NETSIM_DISABLED_IN_DEVELOP) || (!DEBUG && UNITY_MP_TOOLS_NETSIM_ENABLED_IN_RELEASE) #define UNITY_MP_TOOLS_NETSIM_IMPLEMENTATION_ENABLED #endif diff --git a/testproject/Assets/Tests/Manual/SceneTransitioningAdditive/SceneTransitioningBase1.unity b/testproject/Assets/Tests/Manual/SceneTransitioningAdditive/SceneTransitioningBase1.unity index bab567858b..e1dfeb1a4d 100644 --- a/testproject/Assets/Tests/Manual/SceneTransitioningAdditive/SceneTransitioningBase1.unity +++ b/testproject/Assets/Tests/Manual/SceneTransitioningAdditive/SceneTransitioningBase1.unity @@ -246,6 +246,7 @@ MonoBehaviour: Ownership: 1 AlwaysReplicateAsRoot: 0 SynchronizeTransform: 1 + k__BackingField: 0 ActiveSceneSynchronization: 0 SceneMigrationSynchronization: 1 SpawnWithObservers: 1 @@ -278,14 +279,14 @@ Light: m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 37242881} m_Enabled: 1 - serializedVersion: 11 + serializedVersion: 13 m_Type: 1 m_Color: {r: 1, g: 0.95686275, b: 0.8392157, a: 1} m_Intensity: 1 m_Range: 10 m_SpotAngle: 30 m_InnerSpotAngle: 21.80208 - m_CookieSize: 10 + m_CookieSize2D: {x: 10, y: 10} m_Shadows: m_Type: 2 m_Resolution: -1 @@ -330,7 +331,7 @@ Light: m_UseBoundingSphereOverride: 0 m_UseViewFrustumForShadowCasterCull: 1 m_ForceVisible: 0 - m_ShadowRadius: 0 + m_ShapeRadius: 0 m_ShadowAngle: 0 m_LightUnit: 1 m_LuxAtDistance: 1 @@ -646,6 +647,8 @@ MeshRenderer: m_RayTracingAccelStructBuildFlagsOverride: 0 m_RayTracingAccelStructBuildFlags: 1 m_SmallMeshCulling: 1 + m_ForceMeshLod: -1 + m_MeshLodSelectionBias: 0 m_RenderingLayerMask: 1 m_RendererPriority: 0 m_Materials: @@ -667,9 +670,11 @@ MeshRenderer: m_AutoUVMaxDistance: 0.5 m_AutoUVMaxAngle: 89 m_LightmapParameters: {fileID: 0} + m_GlobalIlluminationMeshLod: 0 m_SortingLayerID: 0 m_SortingLayer: 0 m_SortingOrder: 0 + m_MaskInteraction: 0 m_AdditionalVertexStreams: {fileID: 0} --- !u!33 &57392473 MeshFilter: @@ -979,6 +984,7 @@ Canvas: m_OverridePixelPerfect: 0 m_SortingBucketNormalizedSize: 0 m_VertexColorAlwaysGammaSpace: 0 + m_UseReflectionProbes: 0 m_AdditionalShaderChannelsFlag: 0 m_UpdateRectTransformForStandalone: 0 m_SortingLayerID: 0 @@ -1157,6 +1163,7 @@ Canvas: m_OverridePixelPerfect: 0 m_SortingBucketNormalizedSize: 0 m_VertexColorAlwaysGammaSpace: 0 + m_UseReflectionProbes: 0 m_AdditionalShaderChannelsFlag: 0 m_UpdateRectTransformForStandalone: 0 m_SortingLayerID: 0 @@ -1408,6 +1415,8 @@ MeshRenderer: m_RayTracingAccelStructBuildFlagsOverride: 0 m_RayTracingAccelStructBuildFlags: 1 m_SmallMeshCulling: 1 + m_ForceMeshLod: -1 + m_MeshLodSelectionBias: 0 m_RenderingLayerMask: 1 m_RendererPriority: 0 m_Materials: @@ -1429,9 +1438,11 @@ MeshRenderer: m_AutoUVMaxDistance: 0.5 m_AutoUVMaxAngle: 89 m_LightmapParameters: {fileID: 0} + m_GlobalIlluminationMeshLod: 0 m_SortingLayerID: 0 m_SortingLayer: 0 m_SortingOrder: 0 + m_MaskInteraction: 0 m_AdditionalVertexStreams: {fileID: 0} --- !u!33 &336568648 MeshFilter: @@ -2549,7 +2560,7 @@ LightingSettings: m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} m_Name: - serializedVersion: 9 + serializedVersion: 10 m_EnableBakedLightmaps: 1 m_EnableRealtimeLightmaps: 0 m_RealtimeEnvironmentLighting: 1 @@ -2564,6 +2575,12 @@ LightingSettings: m_BakeResolution: 40 m_Padding: 2 m_LightmapCompression: 3 + m_LightmapPackingMode: 1 + m_LightmapPackingMethod: 0 + m_XAtlasPackingAttempts: 16384 + m_XAtlasBruteForce: 0 + m_XAtlasBlockAlign: 0 + m_XAtlasRepackUnderutilizedLightmaps: 1 m_AO: 0 m_AOMaxDistance: 1 m_CompAOExponent: 1 @@ -2876,11 +2893,12 @@ MonoBehaviour: NetworkIdRecycleDelay: 120 RpcHashSize: 0 LoadSceneTimeOut: 120 - SpawnTimeout: 1 + SpawnTimeout: 10 EnableNetworkLogs: 1 NetworkTopology: 0 UseCMBService: 0 AutoSpawnPlayerPrefabClientSide: 1 + NetworkMessageMetrics: 1 NetworkProfilingMetrics: 1 OldPrefabList: - Override: 0 @@ -2934,7 +2952,7 @@ MonoBehaviour: SourceHashToOverride: 0 OverridingTargetPrefab: {fileID: 0} RunInBackground: 1 - LogLevel: 1 + LogLevel: 0 --- !u!4 &1024114720 Transform: m_ObjectHideFlags: 0 @@ -3000,7 +3018,9 @@ MonoBehaviour: ConnectionData: Address: 127.0.0.1 Port: 7777 - ServerListenAddress: + WebSocketPath: / + ServerListenAddress: 127.0.0.1 + ClientBindPort: 0 DebugSimulator: PacketDelayMS: 0 PacketJitterMS: 0 @@ -3088,6 +3108,7 @@ MonoBehaviour: Ownership: 1 AlwaysReplicateAsRoot: 0 SynchronizeTransform: 1 + k__BackingField: 0 ActiveSceneSynchronization: 0 SceneMigrationSynchronization: 1 SpawnWithObservers: 1 @@ -3399,6 +3420,8 @@ MeshRenderer: m_RayTracingAccelStructBuildFlagsOverride: 0 m_RayTracingAccelStructBuildFlags: 1 m_SmallMeshCulling: 1 + m_ForceMeshLod: -1 + m_MeshLodSelectionBias: 0 m_RenderingLayerMask: 1 m_RendererPriority: 0 m_Materials: @@ -3420,9 +3443,11 @@ MeshRenderer: m_AutoUVMaxDistance: 0.5 m_AutoUVMaxAngle: 89 m_LightmapParameters: {fileID: 0} + m_GlobalIlluminationMeshLod: 0 m_SortingLayerID: 0 m_SortingLayer: 0 m_SortingOrder: 0 + m_MaskInteraction: 0 m_AdditionalVertexStreams: {fileID: 0} --- !u!33 &1336081254 MeshFilter: @@ -5318,6 +5343,8 @@ MeshRenderer: m_RayTracingAccelStructBuildFlagsOverride: 0 m_RayTracingAccelStructBuildFlags: 1 m_SmallMeshCulling: 1 + m_ForceMeshLod: -1 + m_MeshLodSelectionBias: 0 m_RenderingLayerMask: 1 m_RendererPriority: 0 m_Materials: @@ -5339,9 +5366,11 @@ MeshRenderer: m_AutoUVMaxDistance: 0.5 m_AutoUVMaxAngle: 89 m_LightmapParameters: {fileID: 0} + m_GlobalIlluminationMeshLod: 0 m_SortingLayerID: 0 m_SortingLayer: 0 m_SortingOrder: 0 + m_MaskInteraction: 0 m_AdditionalVertexStreams: {fileID: 0} --- !u!33 &1857685346 MeshFilter: @@ -5765,6 +5794,8 @@ MeshRenderer: m_RayTracingAccelStructBuildFlagsOverride: 0 m_RayTracingAccelStructBuildFlags: 1 m_SmallMeshCulling: 1 + m_ForceMeshLod: -1 + m_MeshLodSelectionBias: 0 m_RenderingLayerMask: 1 m_RendererPriority: 0 m_Materials: @@ -5786,9 +5817,11 @@ MeshRenderer: m_AutoUVMaxDistance: 0.5 m_AutoUVMaxAngle: 89 m_LightmapParameters: {fileID: 0} + m_GlobalIlluminationMeshLod: 0 m_SortingLayerID: 0 m_SortingLayer: 0 m_SortingOrder: 0 + m_MaskInteraction: 0 m_AdditionalVertexStreams: {fileID: 0} --- !u!33 &2028091271 MeshFilter: @@ -6134,6 +6167,7 @@ MonoBehaviour: Ownership: 1 AlwaysReplicateAsRoot: 0 SynchronizeTransform: 1 + k__BackingField: 0 ActiveSceneSynchronization: 0 SceneMigrationSynchronization: 1 SpawnWithObservers: 1 From 7215237b763069b1c69945ac4ce80df6849a1ccc Mon Sep 17 00:00:00 2001 From: Netcode team bot Date: Sun, 21 Jun 2026 16:07:40 +0200 Subject: [PATCH 15/26] chore: Updated aspects of Netcode package in anticipation of v2.13.0 release (#4033) --- com.unity.netcode.gameobjects/CHANGELOG.md | 17 ++++++++++++++--- com.unity.netcode.gameobjects/package.json | 2 +- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/com.unity.netcode.gameobjects/CHANGELOG.md b/com.unity.netcode.gameobjects/CHANGELOG.md index 3493b13666..1f7af6d540 100644 --- a/com.unity.netcode.gameobjects/CHANGELOG.md +++ b/com.unity.netcode.gameobjects/CHANGELOG.md @@ -10,15 +10,12 @@ Additional documentation and release notes are available at [Multiplayer Documen ### Added -- Added support for Unity's Fast Enter Play Mode with domain reload disabled. (#3956) ### Changed -- Changed replaced define usages of `DEVELOPMENT_BUILD || UNITY_EDITOR` and a few niche uses of `DEVELOPMENT_BUILD` with `DEBUG`. (#4006) ### Deprecated -- Deprecated the nullable boolean `NetworkObject.IsSceneObject` and introduced `NetworkObject.InScenePlaced`. (#4000) ### Removed @@ -32,6 +29,20 @@ Additional documentation and release notes are available at [Multiplayer Documen ### Obsolete +## [2.13.0] - 2026-06-21 + +### Added + +- Added support for Unity's Fast Enter Play Mode with domain reload disabled. (#3956) + +### Changed + +- Changed replaced define usages of `DEVELOPMENT_BUILD || UNITY_EDITOR` and a few niche uses of `DEVELOPMENT_BUILD` with `DEBUG`. (#4006) + +### Deprecated + +- Deprecated the nullable boolean `NetworkObject.IsSceneObject` and introduced `NetworkObject.InScenePlaced`. (#4000) + ## [2.12.0] - 2026-05-24 ### Added diff --git a/com.unity.netcode.gameobjects/package.json b/com.unity.netcode.gameobjects/package.json index d2b5daeecd..074afe6e47 100644 --- a/com.unity.netcode.gameobjects/package.json +++ b/com.unity.netcode.gameobjects/package.json @@ -2,7 +2,7 @@ "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": "2.13.0", + "version": "2.13.1", "unity": "6000.0", "dependencies": { "com.unity.nuget.mono-cecil": "1.11.4", From b776c5446373a4ea39d5ff16d9c2cadf86cbd697 Mon Sep 17 00:00:00 2001 From: Noel Stephens Date: Sun, 21 Jun 2026 10:19:06 -0500 Subject: [PATCH 16/26] fix: NetworkRigidbodyBase 2D rigid body issues (#4012) * fix Fixing issue where Rigidbody2D was not applying rotation correctly. (4009) Fixing issue where `NetworkRigidbodyBase` was always checking the 3D rigid body's interpolation mode when determining if it is kinematic and needs to put the rigid body to sleep and then switch to interpolation. (3924) * style Removing static using directive (not used). * update Missed the return... * test Did a bit of an overhaul on the original NetworkRigidbodyTest. It should now run with distributed authority tests and includes a 2D rigidbody in the tests as well. * stye Removing trailing space on comment * test Updating the NetworkRigidbodyTest to do a 2nd spawn pass that swaps the session owner and non-session owner clients to validate it works when a non-session authority client is the spawn authority. * update Removing Rigidbody tests no longer used and already covered by `NetworkRigidbodyTests`. Moving `RigidbodyContactEventManagerTests` into its own file. --- .../Components/NetworkRigidBodyBase.cs | 62 +- .../Runtime/Physics/NetworkRigidbody2DTest.cs | 80 --- .../Physics/NetworkRigidbody2DTest.cs.meta | 11 - .../Runtime/Physics/NetworkRigidbodyTest.cs | 589 +++++------------- .../RigidbodyContactEventManagerTests.cs | 403 ++++++++++++ .../RigidbodyContactEventManagerTests.cs.meta | 2 + testproject/Assets/Tests/Runtime/Physics.meta | 8 - ...etworkRigidbody2DCntChangeOwnershipTest.cs | 95 --- ...kRigidbody2DCntChangeOwnershipTest.cs.meta | 11 - .../Physics/NetworkRigidbody2DCntTest.cs | 82 --- .../Physics/NetworkRigidbody2DCntTest.cs.meta | 11 - .../NetworkRigidbodyCntChangeOwnershipTest.cs | 95 --- ...orkRigidbodyCntChangeOwnershipTest.cs.meta | 11 - .../Physics/NetworkRigidbodyCntTest.cs | 82 --- .../Physics/NetworkRigidbodyCntTest.cs.meta | 11 - 15 files changed, 613 insertions(+), 940 deletions(-) delete mode 100644 com.unity.netcode.gameobjects/Tests/Runtime/Physics/NetworkRigidbody2DTest.cs delete mode 100644 com.unity.netcode.gameobjects/Tests/Runtime/Physics/NetworkRigidbody2DTest.cs.meta create mode 100644 com.unity.netcode.gameobjects/Tests/Runtime/Physics/RigidbodyContactEventManagerTests.cs create mode 100644 com.unity.netcode.gameobjects/Tests/Runtime/Physics/RigidbodyContactEventManagerTests.cs.meta delete mode 100644 testproject/Assets/Tests/Runtime/Physics.meta delete mode 100644 testproject/Assets/Tests/Runtime/Physics/NetworkRigidbody2DCntChangeOwnershipTest.cs delete mode 100644 testproject/Assets/Tests/Runtime/Physics/NetworkRigidbody2DCntChangeOwnershipTest.cs.meta delete mode 100644 testproject/Assets/Tests/Runtime/Physics/NetworkRigidbody2DCntTest.cs delete mode 100644 testproject/Assets/Tests/Runtime/Physics/NetworkRigidbody2DCntTest.cs.meta delete mode 100644 testproject/Assets/Tests/Runtime/Physics/NetworkRigidbodyCntChangeOwnershipTest.cs delete mode 100644 testproject/Assets/Tests/Runtime/Physics/NetworkRigidbodyCntChangeOwnershipTest.cs.meta delete mode 100644 testproject/Assets/Tests/Runtime/Physics/NetworkRigidbodyCntTest.cs delete mode 100644 testproject/Assets/Tests/Runtime/Physics/NetworkRigidbodyCntTest.cs.meta diff --git a/com.unity.netcode.gameobjects/Runtime/Components/NetworkRigidBodyBase.cs b/com.unity.netcode.gameobjects/Runtime/Components/NetworkRigidBodyBase.cs index 0d0de9afa8..4094e084a8 100644 --- a/com.unity.netcode.gameobjects/Runtime/Components/NetworkRigidBodyBase.cs +++ b/com.unity.netcode.gameobjects/Runtime/Components/NetworkRigidBodyBase.cs @@ -2,6 +2,7 @@ using System.Runtime.CompilerServices; using UnityEngine; + namespace Unity.Netcode.Components { /// @@ -571,7 +572,7 @@ private void InternalMoveRotation2D(Quaternion rotation) { var quaternion = Quaternion.identity; var angles = quaternion.eulerAngles; - angles.z = m_InternalRigidbody2D.rotation; + angles.z = rotation.z; quaternion.eulerAngles = angles; m_InternalRigidbody2D.MoveRotation(quaternion); } @@ -845,6 +846,28 @@ public void SetIsKinematic(bool isKinematic) PostSetIsKinematic(); } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private bool HasInterpolationTypeSet(InterpolationTypes interpolationType) + { +#if COM_UNITY_MODULES_PHYSICS && COM_UNITY_MODULES_PHYSICS2D + if (m_IsRigidbody2D) + { + return interpolationType == InterpolationTypes.Extrapolate ? m_InternalRigidbody2D.interpolation == RigidbodyInterpolation2D.Extrapolate : m_InternalRigidbody2D.interpolation == RigidbodyInterpolation2D.Interpolate; + } + else + { + return interpolationType == InterpolationTypes.Extrapolate ? m_InternalRigidbody.interpolation == RigidbodyInterpolation.Extrapolate : m_InternalRigidbody.interpolation == RigidbodyInterpolation.Interpolate; + } +#endif +#if COM_UNITY_MODULES_PHYSICS && !COM_UNITY_MODULES_PHYSICS2D + return interpolationType == InterpolationTypes.Extrapolate ? m_InternalRigidbody2D.interpolation == RigidbodyInterpolation2D.Extrapolate : m_InternalRigidbody2D.interpolation == RigidbodyInterpolation2D.Interpolate; +#endif +#if !COM_UNITY_MODULES_PHYSICS && COM_UNITY_MODULES_PHYSICS2D + return interpolationType == InterpolationTypes.Extrapolate ? m_InternalRigidbody.interpolation == RigidbodyInterpolation.Extrapolate : m_InternalRigidbody.interpolation == RigidbodyInterpolation.Interpolate; +#endif + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] private void PostSetIsKinematic() { @@ -853,26 +876,29 @@ private void PostSetIsKinematic() { return; } + if (UseRigidBodyForMotion) { - // Only if the NetworkTransform is set to interpolate do we need to check for extrapolation - if (NetworkTransform.Interpolate && m_OriginalInterpolation == InterpolationTypes.Extrapolate) + // Exit early if the original interpolation type is not set to extrapolate or NetworkTransform interpolate is disabled + if (m_OriginalInterpolation != InterpolationTypes.Extrapolate || !NetworkTransform.Interpolate) { - if (IsKinematic()) - { - // If not already set to interpolate then set the Rigidbody to interpolate - if (m_InternalRigidbody.interpolation == RigidbodyInterpolation.Extrapolate) - { - // Sleep until the next fixed update when switching from extrapolation to interpolation - SleepRigidbody(); - SetInterpolation(InterpolationTypes.Interpolate); - } - } - else - { - // Switch it back to the original interpolation if non-kinematic (doesn't require sleep). - SetInterpolation(m_OriginalInterpolation); - } + return; + } + + // Otherwise, if this is the active physics object + if (!IsKinematic()) + { + // switch it back to the original interpolation and exit early + SetInterpolation(m_OriginalInterpolation); + return; + } + + // If the Rigidbody or Rigidbody2D is currently configured to extrapolate + if (HasInterpolationTypeSet(InterpolationTypes.Extrapolate)) + { + // sleep until the next fixed update when switching from extrapolation to interpolation + SleepRigidbody(); + SetInterpolation(InterpolationTypes.Interpolate); } } else diff --git a/com.unity.netcode.gameobjects/Tests/Runtime/Physics/NetworkRigidbody2DTest.cs b/com.unity.netcode.gameobjects/Tests/Runtime/Physics/NetworkRigidbody2DTest.cs deleted file mode 100644 index 089eeee542..0000000000 --- a/com.unity.netcode.gameobjects/Tests/Runtime/Physics/NetworkRigidbody2DTest.cs +++ /dev/null @@ -1,80 +0,0 @@ -#if COM_UNITY_MODULES_PHYSICS2D -using System.Collections; -using NUnit.Framework; -using Unity.Netcode.Components; -using UnityEngine; -using UnityEngine.TestTools; -using Unity.Netcode.TestHelpers.Runtime; - -namespace Unity.Netcode.RuntimeTests -{ - internal class NetworkRigidbody2DDynamicTest : NetworkRigidbody2DTestBase - { - public override bool Kinematic => false; - } - - internal class NetworkRigidbody2DKinematicTest : NetworkRigidbody2DTestBase - { - public override bool Kinematic => true; - } - - public abstract class NetworkRigidbody2DTestBase : NetcodeIntegrationTest - { - protected override int NumberOfClients => 1; - - public abstract bool Kinematic { get; } - - protected override void OnCreatePlayerPrefab() - { - m_PlayerPrefab.AddComponent(); - m_PlayerPrefab.AddComponent(); - m_PlayerPrefab.AddComponent(); - m_PlayerPrefab.GetComponent().interpolation = RigidbodyInterpolation2D.Interpolate; - m_PlayerPrefab.GetComponent().isKinematic = Kinematic; - } - - /// - /// Tests that a server can destroy a NetworkObject and that it gets despawned correctly. - /// - /// - [UnityTest] - public IEnumerator TestRigidbodyKinematicEnableDisable() - { - // This is the *SERVER VERSION* of the *CLIENT PLAYER* - var serverClientPlayerResult = new NetcodeIntegrationTestHelpers.ResultWrapper(); - yield return NetcodeIntegrationTestHelpers.GetNetworkObjectByRepresentation((x => x.IsPlayerObject && x.OwnerClientId == m_ClientNetworkManagers[0].LocalClientId), m_ServerNetworkManager, serverClientPlayerResult); - var serverPlayer = serverClientPlayerResult.Result.gameObject; - - // This is the *CLIENT VERSION* of the *CLIENT PLAYER* - var clientClientPlayerResult = new NetcodeIntegrationTestHelpers.ResultWrapper(); - yield return NetcodeIntegrationTestHelpers.GetNetworkObjectByRepresentation((x => x.IsPlayerObject && x.OwnerClientId == m_ClientNetworkManagers[0].LocalClientId), m_ClientNetworkManagers[0], clientClientPlayerResult); - var clientPlayer = clientClientPlayerResult.Result.gameObject; - - Assert.IsNotNull(serverPlayer); - Assert.IsNotNull(clientPlayer); - - yield return WaitForTicks(m_ServerNetworkManager, 5); - - // server rigidbody has authority and should have a kinematic mode of false - Assert.True(serverPlayer.GetComponent().isKinematic == Kinematic); - Assert.AreEqual(RigidbodyInterpolation2D.Interpolate, serverPlayer.GetComponent().interpolation); - - // client rigidbody has no authority and should have a kinematic mode of true - Assert.True(clientPlayer.GetComponent().isKinematic); - Assert.AreEqual(RigidbodyInterpolation2D.None, clientPlayer.GetComponent().interpolation); - - // despawn the server player, (but keep it around on the server) - serverPlayer.GetComponent().Despawn(false); - - yield return WaitForTicks(m_ServerNetworkManager, 5); - - // This should equal Kinematic - Assert.IsTrue(serverPlayer.GetComponent().isKinematic == Kinematic); - - yield return WaitForTicks(m_ServerNetworkManager, 5); - - Assert.IsTrue(clientPlayer == null); // safety check that object is actually despawned. - } - } -} -#endif // COM_UNITY_MODULES_PHYSICS2D diff --git a/com.unity.netcode.gameobjects/Tests/Runtime/Physics/NetworkRigidbody2DTest.cs.meta b/com.unity.netcode.gameobjects/Tests/Runtime/Physics/NetworkRigidbody2DTest.cs.meta deleted file mode 100644 index 48faa513f7..0000000000 --- a/com.unity.netcode.gameobjects/Tests/Runtime/Physics/NetworkRigidbody2DTest.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: f33b2f298cbb7b248b2a76ba48ee1d53 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/com.unity.netcode.gameobjects/Tests/Runtime/Physics/NetworkRigidbodyTest.cs b/com.unity.netcode.gameobjects/Tests/Runtime/Physics/NetworkRigidbodyTest.cs index 4509797369..1a7d768831 100644 --- a/com.unity.netcode.gameobjects/Tests/Runtime/Physics/NetworkRigidbodyTest.cs +++ b/com.unity.netcode.gameobjects/Tests/Runtime/Physics/NetworkRigidbodyTest.cs @@ -1,7 +1,6 @@ #if COM_UNITY_MODULES_PHYSICS || COM_UNITY_MODULES_PHYSICS2D using System.Collections; using System.Collections.Generic; -using System.Text; using NUnit.Framework; using Unity.Netcode.Components; using Unity.Netcode.TestHelpers.Runtime; @@ -10,487 +9,227 @@ namespace Unity.Netcode.RuntimeTests { - [TestFixture(RigidbodyInterpolation.Interpolate, true, true)] // This should be allowed under all condistions when using Rigidbody motion - [TestFixture(RigidbodyInterpolation.Extrapolate, true, true)] // This should not allow extrapolation on non-auth instances when using Rigidbody motion & NT interpolation - [TestFixture(RigidbodyInterpolation.Extrapolate, false, true)] // This should allow extrapolation on non-auth instances when using Rigidbody & NT has no interpolation - [TestFixture(RigidbodyInterpolation.Interpolate, true, false)] // This should not allow kinematic instances to have Rigidbody interpolation enabled - [TestFixture(RigidbodyInterpolation.Interpolate, false, false)] // Testing that rigid body interpolation remains the same if NT interpolate is disabled + [TestFixture(HostOrServer.Server)] + [TestFixture(HostOrServer.Host)] + [TestFixture(HostOrServer.DAHost)] internal class NetworkRigidbodyTest : NetcodeIntegrationTest { protected override int NumberOfClients => 1; - private bool m_NetworkTransformInterpolate; - private bool m_UseRigidBodyForMotion; - private RigidbodyInterpolation m_RigidbodyInterpolation; - public NetworkRigidbodyTest(RigidbodyInterpolation rigidbodyInterpolation, bool networkTransformInterpolate, bool useRigidbodyForMotion) - { - m_RigidbodyInterpolation = rigidbodyInterpolation; - m_NetworkTransformInterpolate = networkTransformInterpolate; - m_UseRigidBodyForMotion = useRigidbodyForMotion; - } - - protected override void OnCreatePlayerPrefab() - { - var networkTransform = m_PlayerPrefab.AddComponent(); - networkTransform.Interpolate = m_NetworkTransformInterpolate; - var rigidbody = m_PlayerPrefab.AddComponent(); - rigidbody.interpolation = m_RigidbodyInterpolation; - var networkRigidbody = m_PlayerPrefab.AddComponent(); - networkRigidbody.UseRigidBodyForMotion = m_UseRigidBodyForMotion; - } - - /// - /// Tests that a server can destroy a NetworkObject and that it gets despawned correctly. - /// - /// - [UnityTest] - public IEnumerator TestRigidbodyKinematicEnableDisable() - { - // This is the *SERVER VERSION* of the *CLIENT PLAYER* - var serverClientPlayerInstance = m_ServerNetworkManager.ConnectedClients[m_ClientNetworkManagers[0].LocalClientId].PlayerObject; - - // This is the *CLIENT VERSION* of the *CLIENT PLAYER* - var clientPlayerInstance = m_ClientNetworkManagers[0].LocalClient.PlayerObject; - - Assert.IsNotNull(serverClientPlayerInstance, $"{nameof(serverClientPlayerInstance)} is null!"); - Assert.IsNotNull(clientPlayerInstance, $"{nameof(clientPlayerInstance)} is null!"); - - var serverClientInstanceRigidBody = serverClientPlayerInstance.GetComponent(); - var clientRigidBody = clientPlayerInstance.GetComponent(); - - if (m_UseRigidBodyForMotion) - { - var interpolateCompareNonAuthoritative = m_NetworkTransformInterpolate ? RigidbodyInterpolation.Interpolate : m_RigidbodyInterpolation; - - // Server authoritative NT should yield non-kinematic mode for the server-side player instance - Assert.False(serverClientInstanceRigidBody.isKinematic, $"[Server-Side] Client-{m_ClientNetworkManagers[0].LocalClientId} player's {nameof(Rigidbody)} is kinematic!"); - - // The authoritative instance can be None, Interpolate, or Extrapolate for the Rigidbody interpolation settings. - Assert.AreEqual(m_RigidbodyInterpolation, serverClientInstanceRigidBody.interpolation, $"[Server-Side] Client-{m_ClientNetworkManagers[0].LocalClientId} " + - $"player's {nameof(Rigidbody)}'s interpolation is {serverClientInstanceRigidBody.interpolation} and not {m_RigidbodyInterpolation}!"); - - // Server authoritative NT should yield kinematic mode for the client-side player instance - Assert.True(clientRigidBody.isKinematic, $"[Client-Side] Client-{m_ClientNetworkManagers[0].LocalClientId} player's {nameof(Rigidbody)} is not kinematic!"); - - // When using Rigidbody motion, authoritative and non-authoritative Rigidbody interpolation settings should be preserved (except when extrapolation is used - Assert.AreEqual(interpolateCompareNonAuthoritative, clientRigidBody.interpolation, $"[Client-Side] Client-{m_ClientNetworkManagers[0].LocalClientId} " + - $"player's {nameof(Rigidbody)}'s interpolation is {clientRigidBody.interpolation} and not {interpolateCompareNonAuthoritative}!"); - } - else + private List<(RigidbodyInterpolation interpolationType, bool enableInterpolation, bool useRigidbodyForMotion)> m_TestConfigurations = + new List<(RigidbodyInterpolation interpolationType, bool enableInterpolation, bool useRigidbodyForMotion)>() { - // server rigidbody has authority and should not be kinematic - Assert.False(serverClientInstanceRigidBody.isKinematic, $"[Server-Side] Client-{m_ClientNetworkManagers[0].LocalClientId} player's {nameof(Rigidbody)} is kinematic!"); - Assert.AreEqual(RigidbodyInterpolation.Interpolate, serverClientInstanceRigidBody.interpolation, $"[Server-Side] Client-{m_ClientNetworkManagers[0].LocalClientId} " + - $"player's {nameof(Rigidbody)}'s interpolation is {serverClientInstanceRigidBody.interpolation} and not {nameof(RigidbodyInterpolation.Interpolate)}!"); - - // Server authoritative NT should yield kinematic mode for the client-side player instance - Assert.True(clientRigidBody.isKinematic, $"[Client-Side] Client-{m_ClientNetworkManagers[0].LocalClientId} player's {nameof(Rigidbody)} is not kinematic!"); - - // client rigidbody has no authority with NT interpolation disabled should allow Rigidbody interpolation - if (!m_NetworkTransformInterpolate) - { - Assert.AreEqual(RigidbodyInterpolation.Interpolate, clientRigidBody.interpolation, $"[Client-Side] Client-{m_ClientNetworkManagers[0].LocalClientId} " + - $"player's {nameof(Rigidbody)}'s interpolation is {clientRigidBody.interpolation} and not {nameof(RigidbodyInterpolation.Interpolate)}!"); - } - else - { - Assert.AreEqual(RigidbodyInterpolation.None, clientRigidBody.interpolation, $"[Client-Side] Client-{m_ClientNetworkManagers[0].LocalClientId} " + - $"player's {nameof(Rigidbody)}'s interpolation is {clientRigidBody.interpolation} and not {nameof(RigidbodyInterpolation.None)}!"); - } - } - - // despawn the server player (but keep it around on the server) - serverClientPlayerInstance.Despawn(false); - - yield return WaitForConditionOrTimeOut(() => !serverClientPlayerInstance.IsSpawned && !clientPlayerInstance.IsSpawned); - AssertOnTimeout("Timed out waiting for client player to despawn on both server and client!"); - - // When despawned, we should always be kinematic (i.e. don't apply physics when despawned) - Assert.True(serverClientInstanceRigidBody.isKinematic, $"[Server-Side][Despawned] Client-{m_ClientNetworkManagers[0].LocalClientId} player's {nameof(Rigidbody)} is not kinematic when despawned!"); - Assert.IsTrue(clientPlayerInstance == null, $"[Client-Side] Player {nameof(NetworkObject)} is not null!"); - } - } - - internal class ContactEventTransformHelperWithInfo : ContactEventTransformHelper, IContactEventHandlerWithInfo - { - public ContactEventHandlerInfo GetContactEventHandlerInfo() - { - var contactEventHandlerInfo = new ContactEventHandlerInfo() - { - HasContactEventPriority = IsOwner, - ProvideNonRigidBodyContactEvents = m_EnableNonRigidbodyContacts.Value, + (RigidbodyInterpolation.Interpolate, true, true), // This should be allowed under all condistions when using Rigidbody motion + (RigidbodyInterpolation.Extrapolate, true, true), // This should not allow extrapolation on non-auth instances when using Rigidbody motion & NT interpolation + (RigidbodyInterpolation.Extrapolate, false, true), // This should allow extrapolation on non-auth instances when using Rigidbody & NT has no interpolation + (RigidbodyInterpolation.Interpolate, true, false), // This should not allow kinematic instances to have Rigidbody interpolation enabled + (RigidbodyInterpolation.Interpolate, false, false) // Testing that rigidbody interpolation remains the same if NT interpolate is disabled }; - return contactEventHandlerInfo; - } - - protected override void OnRegisterForContactEvents(bool isRegistering) - { - RigidbodyContactEventManager.Instance.RegisterHandler(this, isRegistering); - } - } + /// + /// The current test configuration applied to the current test running. + /// + private (RigidbodyInterpolation interpolationType, bool enableInterpolation, bool useRigidbodyForMotion) m_CurrentConfiguration; - internal class ContactEventTransformHelper : NetworkTransform, IContactEventHandler - { - public static Vector3 SessionOwnerSpawnPoint; - public static Vector3 ClientSpawnPoint; - public static bool VerboseDebug; - public enum HelperStates + public NetworkRigidbodyTest(HostOrServer hostOrServer) : base(hostOrServer) { - None, - MoveForward, } - private HelperStates m_HelperState; - - public void SetHelperState(HelperStates state) - { - m_HelperState = state; - if (!m_NetworkRigidbody.IsKinematic()) - { - m_NetworkRigidbody.Rigidbody.angularVelocity = Vector3.zero; - m_NetworkRigidbody.Rigidbody.linearVelocity = Vector3.zero; - } - m_NetworkRigidbody.Rigidbody.isKinematic = m_HelperState == HelperStates.None; - if (!m_NetworkRigidbody.IsKinematic()) - { - m_NetworkRigidbody.Rigidbody.angularVelocity = Vector3.zero; - m_NetworkRigidbody.Rigidbody.linearVelocity = Vector3.zero; - } + /// + /// Base prefab for and + /// + private GameObject m_RigidbodyPrefab; + private NetworkTransform m_3DNetworkTransform; + private Rigidbody m_PrefabRigidbody; + private NetworkRigidbody m_PrefabNetworkRigidbody; + private NetworkObject m_3DAuthorityInstance; - } + /// + /// Base prefab for and + /// + private GameObject m_Rigidbody2DPrefab; + private NetworkTransform m_2DNetworkTransform; + private Rigidbody2D m_PrefabRigidbody2D; + private NetworkRigidbody2D m_PrefabNetworkRigidbody2D; + private NetworkObject m_2DAuthorityInstance; - protected struct ContactEventInfo + protected override void OnServerAndClientsCreated() { - public ulong EventId; - public Vector3 AveragedCollisionNormal; - public Rigidbody CollidingBody; - public Vector3 ContactPoint; - } + m_RigidbodyPrefab = CreateNetworkObjectPrefab("RBTest"); + m_3DNetworkTransform = m_RigidbodyPrefab.AddComponent(); + m_PrefabRigidbody = m_RigidbodyPrefab.AddComponent(); + m_PrefabNetworkRigidbody = m_RigidbodyPrefab.AddComponent(); - protected List m_ContactEvents = new List(); + m_Rigidbody2DPrefab = CreateNetworkObjectPrefab("RB2DTest"); + m_2DNetworkTransform = m_Rigidbody2DPrefab.AddComponent(); + m_PrefabRigidbody2D = m_Rigidbody2DPrefab.AddComponent(); + m_PrefabNetworkRigidbody2D = m_Rigidbody2DPrefab.AddComponent(); - protected NetworkVariable m_EnableNonRigidbodyContacts = new NetworkVariable(false, NetworkVariableReadPermission.Everyone, NetworkVariableWritePermission.Owner); - - protected NetworkRigidbody m_NetworkRigidbody; - public ContactEventTransformHelper Target; - - public bool HasContactEvents() - { - return m_ContactEvents.Count > 0; + base.OnServerAndClientsCreated(); } - public Rigidbody GetRigidbody() + private string m_ConfigHeader; + private void ApplyCurrentTestConfiguration() { - return m_NetworkRigidbody.Rigidbody; - } - - public bool HadContactWith(ContactEventTransformHelper otherObject) - { - if (otherObject == null) - { - return false; - } - foreach (var contactEvent in m_ContactEvents) - { - if (contactEvent.CollidingBody == otherObject.m_NetworkRigidbody.Rigidbody) - { - return true; - } - } - return false; - } + // Configure both 3D and 2D versions based on the current test configuration + m_3DNetworkTransform.Interpolate = m_CurrentConfiguration.enableInterpolation; + m_PrefabRigidbody.interpolation = m_CurrentConfiguration.interpolationType; + m_PrefabNetworkRigidbody.UseRigidBodyForMotion = m_CurrentConfiguration.useRigidbodyForMotion; + m_2DNetworkTransform.Interpolate = m_CurrentConfiguration.enableInterpolation; + m_PrefabRigidbody2D.interpolation = m_CurrentConfiguration.interpolationType == RigidbodyInterpolation.Interpolate ? RigidbodyInterpolation2D.Interpolate : RigidbodyInterpolation2D.Extrapolate; + m_PrefabNetworkRigidbody2D.UseRigidBodyForMotion = m_CurrentConfiguration.useRigidbodyForMotion; - protected virtual void CheckToStopMoving() - { - SetHelperState(HadContactWith(Target) ? HelperStates.None : HelperStates.MoveForward); + // Build a header used in assert messages + m_ConfigHeader = $"[{m_CurrentConfiguration.interpolationType}][Interpolate: {m_CurrentConfiguration.enableInterpolation}][RB-Motion: {m_CurrentConfiguration.useRigidbodyForMotion}]"; } - public void ContactEvent(ulong eventId, Vector3 averagedCollisionNormal, Rigidbody collidingBody, Vector3 contactPoint, bool hasCollisionStay = false, Vector3 averagedCollisionStayNormal = default) + /// + /// Iterates through the to validate various + /// Rigidbody interpolation settings and kinematic states for authority and non-authority + /// instances. + /// + [UnityTest] + public IEnumerator TestRigidbodyKinematicEnableDisable() { - if (Target == null) + foreach (var configuration in m_TestConfigurations) { - return; - } + m_CurrentConfiguration = configuration; + ApplyCurrentTestConfiguration(); - if (collidingBody != null) - { - Log($">>>>>>> contact event with {collidingBody.name}!"); - } - else - { - Log($">>>>>>> contact event with non-rigidbody!"); - } + // Host, Server, DAHost/Session-owner are spawn authority + yield return RunTestConfiguration(); - m_ContactEvents.Add(new ContactEventInfo() - { - EventId = eventId, - AveragedCollisionNormal = averagedCollisionNormal, - CollidingBody = collidingBody, - ContactPoint = contactPoint, - }); - CheckToStopMoving(); - } - - private void SetInitialPositionClientServer() - { - if (IsServer) - { - if (!NetworkManager.DistributedAuthorityMode && !IsLocalPlayer) - { - transform.position = ClientSpawnPoint; - m_NetworkRigidbody.Rigidbody.position = ClientSpawnPoint; - } - else + // When using distributed authority, swap the session owner with + // the non-session owner client as being the spawn authority. + if (m_DistributedAuthority) { - transform.position = SessionOwnerSpawnPoint; - m_NetworkRigidbody.Rigidbody.position = SessionOwnerSpawnPoint; + yield return RunTestConfiguration(true); } } - else - { - transform.position = ClientSpawnPoint; - m_NetworkRigidbody.Rigidbody.position = ClientSpawnPoint; - } } - private void SetInitialPositionDistributedAuthority() - { - if (HasAuthority) + /// + /// Validates the current applied test configuration. + /// + private IEnumerator RunTestConfiguration(bool swapAuthority = false) + { + // The authority is the "spawn authority". + // Distributed authority runs this a second time with a non-session owner client being the + // spawn authority to validate that scenario works correctly. + var authority = !swapAuthority ? GetAuthorityNetworkManager() : GetNonAuthorityNetworkManager(); + var nonAuthority = !swapAuthority ? GetNonAuthorityNetworkManager() : GetAuthorityNetworkManager(); + + // Spawn instances of both the 3D and 2D prefabs configured for the current test. + m_3DAuthorityInstance = SpawnObject(m_RigidbodyPrefab, authority).GetComponent(); + yield return WaitForSpawnedOnAllOrTimeOut(m_3DAuthorityInstance); + AssertOnTimeout($"Failed to spawn {m_3DAuthorityInstance.name} on all clients!"); + + m_2DAuthorityInstance = SpawnObject(m_Rigidbody2DPrefab, authority).GetComponent(); + yield return WaitForSpawnedOnAllOrTimeOut(m_2DAuthorityInstance); + AssertOnTimeout($"Failed to spawn {m_2DAuthorityInstance.name} on all clients!"); + + // Test 3D Rigidbody + #region 3D Rigidbody validation + var authorityRigidbody = m_3DAuthorityInstance.GetComponent(); + var nonAuthorityInstance = nonAuthority.SpawnManager.SpawnedObjects[m_3DAuthorityInstance.NetworkObjectId]; + var nonAuthorityRigidbody = nonAuthorityInstance.GetComponent(); + var authorityHeader = $"{m_ConfigHeader}[Authority] Client-{authority.LocalClientId}'s instance of {m_3DAuthorityInstance.name}"; + // The authority instance should always be non-kinematic + Assert.False(authorityRigidbody.isKinematic, $"{authorityHeader} is kinematic!"); + + var nonAuthorityHeader = $"{m_ConfigHeader}[Non-Authority] Client-{nonAuthority.LocalClientId}'s instance of {nonAuthorityInstance.name}"; + // Non-authority instances should always be kinematic + Assert.True(nonAuthorityRigidbody.isKinematic, $"{nonAuthorityHeader} is not kinematic!"); + var interpolateCompareNonAuthoritative = RigidbodyInterpolation.None; + + if (m_CurrentConfiguration.useRigidbodyForMotion) { - if (IsSessionOwner) - { - transform.position = SessionOwnerSpawnPoint; - m_NetworkRigidbody.Rigidbody.position = SessionOwnerSpawnPoint; - } - else - { - transform.position = ClientSpawnPoint; - m_NetworkRigidbody.Rigidbody.position = ClientSpawnPoint; - } - } - } - - public override void OnNetworkSpawn() - { - m_NetworkRigidbody = GetComponent(); + // The authoritative instance can be None, Interpolate, or Extrapolate for the Rigidbody interpolation settings. + Assert.AreEqual(m_CurrentConfiguration.interpolationType, authorityRigidbody.interpolation, $"{authorityHeader} interpolation is {authorityRigidbody.interpolation} " + + $"and not {m_CurrentConfiguration.interpolationType}!"); - m_NetworkRigidbody.Rigidbody.maxLinearVelocity = 15; - m_NetworkRigidbody.Rigidbody.maxAngularVelocity = 10; + // When using Rigidbody motion, authoritative and non-authoritative Rigidbody interpolation settings should be preserved (except when extrapolation is used + interpolateCompareNonAuthoritative = m_CurrentConfiguration.enableInterpolation ? RigidbodyInterpolation.Interpolate : m_CurrentConfiguration.interpolationType; - if (NetworkManager.DistributedAuthorityMode) - { - SetInitialPositionDistributedAuthority(); } else { - SetInitialPositionClientServer(); - } - if (IsLocalPlayer) - { - RegisterForContactEvents(true); - } - else - { - m_NetworkRigidbody.Rigidbody.detectCollisions = false; - } - base.OnNetworkSpawn(); - } - - protected virtual void OnRegisterForContactEvents(bool isRegistering) - { - RigidbodyContactEventManager.Instance.RegisterHandler(this, isRegistering); - } - - public void RegisterForContactEvents(bool isRegistering) - { - OnRegisterForContactEvents(isRegistering); - } - - private void FixedUpdate() - { - if (!IsSpawned || !IsOwner || m_HelperState != HelperStates.MoveForward) - { - return; - } - var distance = Vector3.Distance(Target.transform.position, transform.position); - var moveAmount = Mathf.Max(1.2f, distance); - // Head towards our target - var dir = (Target.transform.position - transform.position).normalized; - var deltaMove = dir * moveAmount * Time.fixedDeltaTime; - m_NetworkRigidbody.Rigidbody.MovePosition(m_NetworkRigidbody.Rigidbody.position + deltaMove); - + Assert.AreEqual(RigidbodyInterpolation.Interpolate, authorityRigidbody.interpolation, $"{authorityHeader} interpolation is {authorityRigidbody.interpolation} " + + $"and not {RigidbodyInterpolation.Interpolate}!"); - Log($" Loc: {transform.position} | Dest: {Target.transform.position} | Dist: {distance} | MoveDelta: {deltaMove}"); - } - - protected void Log(string msg) - { - if (VerboseDebug) - { - Debug.Log($"Client-{OwnerClientId} {msg}"); + // client rigidbody has no authority with NT interpolation disabled should allow Rigidbody interpolation + interpolateCompareNonAuthoritative = m_CurrentConfiguration.enableInterpolation ? RigidbodyInterpolation.None : RigidbodyInterpolation.Interpolate; } - } - } - - [TestFixture(HostOrServer.Host, ContactEventTypes.Default)] - [TestFixture(HostOrServer.DAHost, ContactEventTypes.Default)] - [TestFixture(HostOrServer.Host, ContactEventTypes.WithInfo)] - [TestFixture(HostOrServer.DAHost, ContactEventTypes.WithInfo)] - internal class RigidbodyContactEventManagerTests : IntegrationTestWithApproximation - { - protected override int NumberOfClients => 1; - - private GameObject m_RigidbodyContactEventManager; - public enum ContactEventTypes - { - Default, - WithInfo - } + Assert.AreEqual(interpolateCompareNonAuthoritative, nonAuthorityRigidbody.interpolation, $"{nonAuthorityHeader} interpolation is {nonAuthorityRigidbody.interpolation} " + + $"and not {interpolateCompareNonAuthoritative}!"); + #endregion - private ContactEventTypes m_ContactEventType; - private StringBuilder m_ErrorLogger = new StringBuilder(); + // Test 2D Rigidbody + #region 2D Rigidbody validation + var authorityRigidbody2D = m_2DAuthorityInstance.GetComponent(); + var nonAuthorityInstance2D = nonAuthority.SpawnManager.SpawnedObjects[m_2DAuthorityInstance.NetworkObjectId]; + var nonAuthorityRigidbody2D = nonAuthorityInstance2D.GetComponent(); - public RigidbodyContactEventManagerTests(HostOrServer hostOrServer, ContactEventTypes contactEventType) : base(hostOrServer) - { - m_ContactEventType = contactEventType; - } + authorityHeader = $"{m_ConfigHeader}[Authority] Client-{authority.LocalClientId}'s instance of {m_2DAuthorityInstance.name}"; + // The authority instance should always be non-kinematic + Assert.False(authorityRigidbody2D.bodyType == RigidbodyType2D.Kinematic, $"{authorityHeader} is kinematic!"); - protected override void OnCreatePlayerPrefab() - { - ContactEventTransformHelper.SessionOwnerSpawnPoint = GetRandomVector3(-4, -3); - ContactEventTransformHelper.ClientSpawnPoint = GetRandomVector3(3, 4); - if (m_ContactEventType == ContactEventTypes.Default) + nonAuthorityHeader = $"{m_ConfigHeader}[Non-Authority] Client-{nonAuthority.LocalClientId}'s instance of {nonAuthorityInstance.name}"; + // Non-authority instances should always be kinematic + Assert.True(nonAuthorityRigidbody2D.bodyType == RigidbodyType2D.Kinematic, $"{nonAuthorityHeader} is not kinematic!"); + var interpolateCompareNonAuthoritative2D = RigidbodyInterpolation2D.None; + var configInterpolation2D = m_CurrentConfiguration.interpolationType == RigidbodyInterpolation.Interpolate ? RigidbodyInterpolation2D.Interpolate : RigidbodyInterpolation2D.Extrapolate; + if (m_CurrentConfiguration.useRigidbodyForMotion) { - var helper = m_PlayerPrefab.AddComponent(); - helper.AuthorityMode = NetworkTransform.AuthorityModes.Owner; + // The authoritative instance can be None, Interpolate, or Extrapolate for the Rigidbody interpolation settings. + Assert.AreEqual(configInterpolation2D, authorityRigidbody2D.interpolation, $"{authorityHeader} interpolation is {authorityRigidbody2D.interpolation} " + + $"and not {m_CurrentConfiguration.interpolationType}!"); + + // When using Rigidbody motion, authoritative and non-authoritative Rigidbody interpolation settings should be preserved (except when extrapolation is used + interpolateCompareNonAuthoritative2D = m_CurrentConfiguration.enableInterpolation ? RigidbodyInterpolation2D.Interpolate : configInterpolation2D; } else { - var helperWithInfo = m_PlayerPrefab.AddComponent(); - helperWithInfo.AuthorityMode = NetworkTransform.AuthorityModes.Owner; - } - - var rigidbody = m_PlayerPrefab.AddComponent(); - rigidbody.useGravity = false; - rigidbody.isKinematic = true; - rigidbody.mass = 5.0f; - rigidbody.collisionDetectionMode = CollisionDetectionMode.Continuous; - var sphereCollider = m_PlayerPrefab.AddComponent(); - sphereCollider.radius = 0.5f; - sphereCollider.providesContacts = true; - - var networkRigidbody = m_PlayerPrefab.AddComponent(); - networkRigidbody.UseRigidBodyForMotion = true; - networkRigidbody.AutoUpdateKinematicState = false; - - m_RigidbodyContactEventManager = new GameObject(); - m_RigidbodyContactEventManager.AddComponent(); - } - + Assert.AreEqual(RigidbodyInterpolation2D.Interpolate, authorityRigidbody2D.interpolation, $"{authorityHeader} interpolation is {authorityRigidbody2D.interpolation} " + + $"and not {RigidbodyInterpolation2D.Interpolate}!"); - - private bool PlayersSpawnedInRightLocation() - { - var authority = GetAuthorityNetworkManager(); - var nonAuthority = GetNonAuthorityNetworkManager(); - - var position = authority.LocalClient.PlayerObject.transform.position; - if (!Approximately(ContactEventTransformHelper.SessionOwnerSpawnPoint, position)) - { - m_ErrorLogger.AppendLine($"Client-{authority.LocalClientId} player position {position} does not match the assigned player position {ContactEventTransformHelper.SessionOwnerSpawnPoint}!"); - return false; - } - - position = nonAuthority.LocalClient.PlayerObject.transform.position; - if (!Approximately(ContactEventTransformHelper.ClientSpawnPoint, position)) - { - m_ErrorLogger.AppendLine($"Client-{nonAuthority.LocalClientId} player position {position} does not match the assigned player position {ContactEventTransformHelper.ClientSpawnPoint}!"); - return false; - } - var playerObject = (NetworkObject)null; - if (!authority.SpawnManager.SpawnedObjects.ContainsKey(nonAuthority.LocalClient.PlayerObject.NetworkObjectId)) - { - m_ErrorLogger.AppendLine($"Client-{authority.LocalClientId} cannot find a local spawned instance of Client-{nonAuthority.LocalClientId}'s player object!"); - return false; + // client rigidbody has no authority with NT interpolation disabled should allow Rigidbody interpolation + interpolateCompareNonAuthoritative2D = m_CurrentConfiguration.enableInterpolation ? RigidbodyInterpolation2D.None : RigidbodyInterpolation2D.Interpolate; } - playerObject = authority.SpawnManager.SpawnedObjects[nonAuthority.LocalClient.PlayerObject.NetworkObjectId]; - position = playerObject.transform.position; - if (!Approximately(ContactEventTransformHelper.ClientSpawnPoint, position)) - { - m_ErrorLogger.AppendLine($"Client-{authority.LocalClientId} player position {position} for Client-{playerObject.OwnerClientId} does not match the assigned player position {ContactEventTransformHelper.ClientSpawnPoint}!"); - return false; - } + Assert.AreEqual(interpolateCompareNonAuthoritative2D, nonAuthorityRigidbody2D.interpolation, $"{nonAuthorityHeader} interpolation is {nonAuthorityRigidbody2D.interpolation} " + + $"and not {interpolateCompareNonAuthoritative}!"); + #endregion - if (!nonAuthority.SpawnManager.SpawnedObjects.ContainsKey(authority.LocalClient.PlayerObject.NetworkObjectId)) - { - m_ErrorLogger.AppendLine($"Client-{nonAuthority.LocalClientId} cannot find a local spawned instance of Client-{authority.LocalClientId}'s player object!"); - return false; - } - playerObject = nonAuthority.SpawnManager.SpawnedObjects[authority.LocalClient.PlayerObject.NetworkObjectId]; - position = playerObject.transform.position; - if (!Approximately(ContactEventTransformHelper.SessionOwnerSpawnPoint, playerObject.transform.position)) - { - m_ErrorLogger.AppendLine($"Client-{nonAuthority.LocalClientId} player position {position} for Client-{playerObject.OwnerClientId} does not match the assigned player position {ContactEventTransformHelper.SessionOwnerSpawnPoint}!"); - return false; - } - return true; + var spawnedInstances = new List() { m_3DAuthorityInstance, m_2DAuthorityInstance }; + m_3DAuthorityInstance.Despawn(); + m_2DAuthorityInstance.Despawn(); + yield return WaitForDespawnedOnAllOrTimeOut(spawnedInstances); + AssertOnTimeout($"Failed to de-spawn instances on all clients!"); + m_3DAuthorityInstance = null; + m_2DAuthorityInstance = null; } - - [UnityTest] - public IEnumerator TestContactEvents() + /// + /// Handle clean up in case of a failed test + /// + protected override IEnumerator OnTearDown() { - ContactEventTransformHelper.VerboseDebug = m_EnableVerboseDebug; - - m_PlayerPrefab.SetActive(false); - m_ErrorLogger.Clear(); - // Validate all instances are spawned in the right location - yield return WaitForConditionOrTimeOut(PlayersSpawnedInRightLocation); - AssertOnTimeout($"Timed out waiting for all player instances to spawn in the corect location:\n {m_ErrorLogger}"); - m_ErrorLogger.Clear(); - - var authority = GetAuthorityNetworkManager(); - var nonAuthority = GetNonAuthorityNetworkManager(); + // If either of these are not null then we most likely failed and didn't cleanup. - var sessionOwnerPlayer = m_ContactEventType == ContactEventTypes.Default ? authority.LocalClient.PlayerObject.GetComponent() : - authority.LocalClient.PlayerObject.GetComponent(); - var clientPlayer = m_ContactEventType == ContactEventTypes.Default ? nonAuthority.LocalClient.PlayerObject.GetComponent() : - nonAuthority.LocalClient.PlayerObject.GetComponent(); - - // Get both players to point towards each other - sessionOwnerPlayer.Target = clientPlayer; - clientPlayer.Target = sessionOwnerPlayer; - - sessionOwnerPlayer.SetHelperState(ContactEventTransformHelper.HelperStates.MoveForward); - clientPlayer.SetHelperState(ContactEventTransformHelper.HelperStates.MoveForward); - - - yield return WaitForConditionOrTimeOut(() => sessionOwnerPlayer.HadContactWith(clientPlayer) || clientPlayer.HadContactWith(sessionOwnerPlayer)); - AssertOnTimeout("Timed out waiting for a player to collide with another player!"); - - clientPlayer.RegisterForContactEvents(false); - sessionOwnerPlayer.RegisterForContactEvents(false); - var otherPlayer = m_ContactEventType == ContactEventTypes.Default ? authority.SpawnManager.SpawnedObjects[clientPlayer.NetworkObjectId].GetComponent() : - authority.SpawnManager.SpawnedObjects[clientPlayer.NetworkObjectId].GetComponent(); - otherPlayer.RegisterForContactEvents(false); - otherPlayer = m_ContactEventType == ContactEventTypes.Default ? nonAuthority.SpawnManager.SpawnedObjects[sessionOwnerPlayer.NetworkObjectId].GetComponent() : - nonAuthority.SpawnManager.SpawnedObjects[sessionOwnerPlayer.NetworkObjectId].GetComponent(); - otherPlayer.RegisterForContactEvents(false); - - Object.Destroy(m_RigidbodyContactEventManager); - m_RigidbodyContactEventManager = null; - } + // Clean-up m_3DAuthorityInstance + if (m_3DAuthorityInstance) + { + Object.Destroy(m_3DAuthorityInstance); + m_3DAuthorityInstance = null; + } - protected override IEnumerator OnTearDown() - { - // In case of a test failure - if (m_RigidbodyContactEventManager) + // Clean-up m_2DAuthorityInstance + if (m_2DAuthorityInstance) { - Object.Destroy(m_RigidbodyContactEventManager); - m_RigidbodyContactEventManager = null; + Object.Destroy(m_2DAuthorityInstance); + m_2DAuthorityInstance = null; } return base.OnTearDown(); diff --git a/com.unity.netcode.gameobjects/Tests/Runtime/Physics/RigidbodyContactEventManagerTests.cs b/com.unity.netcode.gameobjects/Tests/Runtime/Physics/RigidbodyContactEventManagerTests.cs new file mode 100644 index 0000000000..51aaa5ba56 --- /dev/null +++ b/com.unity.netcode.gameobjects/Tests/Runtime/Physics/RigidbodyContactEventManagerTests.cs @@ -0,0 +1,403 @@ +#if COM_UNITY_MODULES_PHYSICS || COM_UNITY_MODULES_PHYSICS2D +using System.Collections; +using System.Collections.Generic; +using System.Text; +using NUnit.Framework; +using Unity.Netcode.Components; +using Unity.Netcode.TestHelpers.Runtime; +using UnityEngine; +using UnityEngine.TestTools; + + +namespace Unity.Netcode.RuntimeTests +{ + + + [TestFixture(HostOrServer.Host, ContactEventTypes.Default)] + [TestFixture(HostOrServer.DAHost, ContactEventTypes.Default)] + [TestFixture(HostOrServer.Host, ContactEventTypes.WithInfo)] + [TestFixture(HostOrServer.DAHost, ContactEventTypes.WithInfo)] + internal class RigidbodyContactEventManagerTests : IntegrationTestWithApproximation + { + protected override int NumberOfClients => 1; + + private GameObject m_RigidbodyContactEventManager; + + public enum ContactEventTypes + { + Default, + WithInfo + } + + private ContactEventTypes m_ContactEventType; + private StringBuilder m_ErrorLogger = new StringBuilder(); + + public RigidbodyContactEventManagerTests(HostOrServer hostOrServer, ContactEventTypes contactEventType) : base(hostOrServer) + { + m_ContactEventType = contactEventType; + } + + protected override void OnCreatePlayerPrefab() + { + ContactEventTransformHelper.SessionOwnerSpawnPoint = GetRandomVector3(-4, -3); + ContactEventTransformHelper.ClientSpawnPoint = GetRandomVector3(3, 4); + if (m_ContactEventType == ContactEventTypes.Default) + { + var helper = m_PlayerPrefab.AddComponent(); + helper.AuthorityMode = NetworkTransform.AuthorityModes.Owner; + } + else + { + var helperWithInfo = m_PlayerPrefab.AddComponent(); + helperWithInfo.AuthorityMode = NetworkTransform.AuthorityModes.Owner; + } + + var rigidbody = m_PlayerPrefab.AddComponent(); + rigidbody.useGravity = false; + rigidbody.isKinematic = true; + rigidbody.mass = 5.0f; + rigidbody.collisionDetectionMode = CollisionDetectionMode.Continuous; + var sphereCollider = m_PlayerPrefab.AddComponent(); + sphereCollider.radius = 0.5f; + sphereCollider.providesContacts = true; + + var networkRigidbody = m_PlayerPrefab.AddComponent(); + networkRigidbody.UseRigidBodyForMotion = true; + networkRigidbody.AutoUpdateKinematicState = false; + + m_RigidbodyContactEventManager = new GameObject(); + m_RigidbodyContactEventManager.AddComponent(); + } + + + + private bool PlayersSpawnedInRightLocation() + { + var authority = GetAuthorityNetworkManager(); + var nonAuthority = GetNonAuthorityNetworkManager(); + + var position = authority.LocalClient.PlayerObject.transform.position; + if (!Approximately(ContactEventTransformHelper.SessionOwnerSpawnPoint, position)) + { + m_ErrorLogger.AppendLine($"Client-{authority.LocalClientId} player position {position} does not match the assigned player position {ContactEventTransformHelper.SessionOwnerSpawnPoint}!"); + return false; + } + + position = nonAuthority.LocalClient.PlayerObject.transform.position; + if (!Approximately(ContactEventTransformHelper.ClientSpawnPoint, position)) + { + m_ErrorLogger.AppendLine($"Client-{nonAuthority.LocalClientId} player position {position} does not match the assigned player position {ContactEventTransformHelper.ClientSpawnPoint}!"); + return false; + } + var playerObject = (NetworkObject)null; + if (!authority.SpawnManager.SpawnedObjects.ContainsKey(nonAuthority.LocalClient.PlayerObject.NetworkObjectId)) + { + m_ErrorLogger.AppendLine($"Client-{authority.LocalClientId} cannot find a local spawned instance of Client-{nonAuthority.LocalClientId}'s player object!"); + return false; + } + playerObject = authority.SpawnManager.SpawnedObjects[nonAuthority.LocalClient.PlayerObject.NetworkObjectId]; + position = playerObject.transform.position; + + if (!Approximately(ContactEventTransformHelper.ClientSpawnPoint, position)) + { + m_ErrorLogger.AppendLine($"Client-{authority.LocalClientId} player position {position} for Client-{playerObject.OwnerClientId} does not match the assigned player position {ContactEventTransformHelper.ClientSpawnPoint}!"); + return false; + } + + if (!nonAuthority.SpawnManager.SpawnedObjects.ContainsKey(authority.LocalClient.PlayerObject.NetworkObjectId)) + { + m_ErrorLogger.AppendLine($"Client-{nonAuthority.LocalClientId} cannot find a local spawned instance of Client-{authority.LocalClientId}'s player object!"); + return false; + } + playerObject = nonAuthority.SpawnManager.SpawnedObjects[authority.LocalClient.PlayerObject.NetworkObjectId]; + position = playerObject.transform.position; + if (!Approximately(ContactEventTransformHelper.SessionOwnerSpawnPoint, playerObject.transform.position)) + { + m_ErrorLogger.AppendLine($"Client-{nonAuthority.LocalClientId} player position {position} for Client-{playerObject.OwnerClientId} does not match the assigned player position {ContactEventTransformHelper.SessionOwnerSpawnPoint}!"); + return false; + } + return true; + } + + + [UnityTest] + public IEnumerator TestContactEvents() + { + ContactEventTransformHelper.VerboseDebug = m_EnableVerboseDebug; + + m_PlayerPrefab.SetActive(false); + m_ErrorLogger.Clear(); + // Validate all instances are spawned in the right location + yield return WaitForConditionOrTimeOut(PlayersSpawnedInRightLocation); + AssertOnTimeout($"Timed out waiting for all player instances to spawn in the corect location:\n {m_ErrorLogger}"); + m_ErrorLogger.Clear(); + + var authority = GetAuthorityNetworkManager(); + var nonAuthority = GetNonAuthorityNetworkManager(); + + var sessionOwnerPlayer = m_ContactEventType == ContactEventTypes.Default ? authority.LocalClient.PlayerObject.GetComponent() : + authority.LocalClient.PlayerObject.GetComponent(); + var clientPlayer = m_ContactEventType == ContactEventTypes.Default ? nonAuthority.LocalClient.PlayerObject.GetComponent() : + nonAuthority.LocalClient.PlayerObject.GetComponent(); + + // Get both players to point towards each other + sessionOwnerPlayer.Target = clientPlayer; + clientPlayer.Target = sessionOwnerPlayer; + + sessionOwnerPlayer.SetHelperState(ContactEventTransformHelper.HelperStates.MoveForward); + clientPlayer.SetHelperState(ContactEventTransformHelper.HelperStates.MoveForward); + + + yield return WaitForConditionOrTimeOut(() => sessionOwnerPlayer.HadContactWith(clientPlayer) || clientPlayer.HadContactWith(sessionOwnerPlayer)); + AssertOnTimeout("Timed out waiting for a player to collide with another player!"); + + clientPlayer.RegisterForContactEvents(false); + sessionOwnerPlayer.RegisterForContactEvents(false); + var otherPlayer = m_ContactEventType == ContactEventTypes.Default ? authority.SpawnManager.SpawnedObjects[clientPlayer.NetworkObjectId].GetComponent() : + authority.SpawnManager.SpawnedObjects[clientPlayer.NetworkObjectId].GetComponent(); + otherPlayer.RegisterForContactEvents(false); + otherPlayer = m_ContactEventType == ContactEventTypes.Default ? nonAuthority.SpawnManager.SpawnedObjects[sessionOwnerPlayer.NetworkObjectId].GetComponent() : + nonAuthority.SpawnManager.SpawnedObjects[sessionOwnerPlayer.NetworkObjectId].GetComponent(); + otherPlayer.RegisterForContactEvents(false); + + Object.Destroy(m_RigidbodyContactEventManager); + m_RigidbodyContactEventManager = null; + } + + protected override IEnumerator OnTearDown() + { + // In case of a test failure + if (m_RigidbodyContactEventManager) + { + Object.Destroy(m_RigidbodyContactEventManager); + m_RigidbodyContactEventManager = null; + } + + return base.OnTearDown(); + } + } + + #region Test helper classes + internal class ContactEventTransformHelperWithInfo : ContactEventTransformHelper, IContactEventHandlerWithInfo + { + public ContactEventHandlerInfo GetContactEventHandlerInfo() + { + var contactEventHandlerInfo = new ContactEventHandlerInfo() + { + HasContactEventPriority = IsOwner, + ProvideNonRigidBodyContactEvents = m_EnableNonRigidbodyContacts.Value, + }; + return contactEventHandlerInfo; + } + + protected override void OnRegisterForContactEvents(bool isRegistering) + { + RigidbodyContactEventManager.Instance.RegisterHandler(this, isRegistering); + } + } + + internal class ContactEventTransformHelper : NetworkTransform, IContactEventHandler + { + public static Vector3 SessionOwnerSpawnPoint; + public static Vector3 ClientSpawnPoint; + public static bool VerboseDebug; + public enum HelperStates + { + None, + MoveForward, + } + + private HelperStates m_HelperState; + + public void SetHelperState(HelperStates state) + { + m_HelperState = state; + if (!m_NetworkRigidbody.IsKinematic()) + { + m_NetworkRigidbody.Rigidbody.angularVelocity = Vector3.zero; + m_NetworkRigidbody.Rigidbody.linearVelocity = Vector3.zero; + } + m_NetworkRigidbody.Rigidbody.isKinematic = m_HelperState == HelperStates.None; + if (!m_NetworkRigidbody.IsKinematic()) + { + m_NetworkRigidbody.Rigidbody.angularVelocity = Vector3.zero; + m_NetworkRigidbody.Rigidbody.linearVelocity = Vector3.zero; + } + + } + + protected struct ContactEventInfo + { + public ulong EventId; + public Vector3 AveragedCollisionNormal; + public Rigidbody CollidingBody; + public Vector3 ContactPoint; + } + + protected List m_ContactEvents = new List(); + + protected NetworkVariable m_EnableNonRigidbodyContacts = new NetworkVariable(false, NetworkVariableReadPermission.Everyone, NetworkVariableWritePermission.Owner); + + protected NetworkRigidbody m_NetworkRigidbody; + public ContactEventTransformHelper Target; + + public bool HasContactEvents() + { + return m_ContactEvents.Count > 0; + } + + public Rigidbody GetRigidbody() + { + return m_NetworkRigidbody.Rigidbody; + } + + public bool HadContactWith(ContactEventTransformHelper otherObject) + { + if (otherObject == null) + { + return false; + } + foreach (var contactEvent in m_ContactEvents) + { + if (contactEvent.CollidingBody == otherObject.m_NetworkRigidbody.Rigidbody) + { + return true; + } + } + return false; + } + + protected virtual void CheckToStopMoving() + { + SetHelperState(HadContactWith(Target) ? HelperStates.None : HelperStates.MoveForward); + } + + public void ContactEvent(ulong eventId, Vector3 averagedCollisionNormal, Rigidbody collidingBody, Vector3 contactPoint, bool hasCollisionStay = false, Vector3 averagedCollisionStayNormal = default) + { + if (Target == null) + { + return; + } + + if (collidingBody != null) + { + Log($">>>>>>> contact event with {collidingBody.name}!"); + } + else + { + Log($">>>>>>> contact event with non-rigidbody!"); + } + + m_ContactEvents.Add(new ContactEventInfo() + { + EventId = eventId, + AveragedCollisionNormal = averagedCollisionNormal, + CollidingBody = collidingBody, + ContactPoint = contactPoint, + }); + CheckToStopMoving(); + } + + private void SetInitialPositionClientServer() + { + if (IsServer) + { + if (!NetworkManager.DistributedAuthorityMode && !IsLocalPlayer) + { + transform.position = ClientSpawnPoint; + m_NetworkRigidbody.Rigidbody.position = ClientSpawnPoint; + } + else + { + transform.position = SessionOwnerSpawnPoint; + m_NetworkRigidbody.Rigidbody.position = SessionOwnerSpawnPoint; + } + } + else + { + transform.position = ClientSpawnPoint; + m_NetworkRigidbody.Rigidbody.position = ClientSpawnPoint; + } + } + + private void SetInitialPositionDistributedAuthority() + { + if (HasAuthority) + { + if (IsSessionOwner) + { + transform.position = SessionOwnerSpawnPoint; + m_NetworkRigidbody.Rigidbody.position = SessionOwnerSpawnPoint; + } + else + { + transform.position = ClientSpawnPoint; + m_NetworkRigidbody.Rigidbody.position = ClientSpawnPoint; + } + } + } + + public override void OnNetworkSpawn() + { + m_NetworkRigidbody = GetComponent(); + + m_NetworkRigidbody.Rigidbody.maxLinearVelocity = 15; + m_NetworkRigidbody.Rigidbody.maxAngularVelocity = 10; + + if (NetworkManager.DistributedAuthorityMode) + { + SetInitialPositionDistributedAuthority(); + } + else + { + SetInitialPositionClientServer(); + } + if (IsLocalPlayer) + { + RegisterForContactEvents(true); + } + else + { + m_NetworkRigidbody.Rigidbody.detectCollisions = false; + } + base.OnNetworkSpawn(); + } + + protected virtual void OnRegisterForContactEvents(bool isRegistering) + { + RigidbodyContactEventManager.Instance.RegisterHandler(this, isRegistering); + } + + public void RegisterForContactEvents(bool isRegistering) + { + OnRegisterForContactEvents(isRegistering); + } + + private void FixedUpdate() + { + if (!IsSpawned || !IsOwner || m_HelperState != HelperStates.MoveForward) + { + return; + } + var distance = Vector3.Distance(Target.transform.position, transform.position); + var moveAmount = Mathf.Max(1.2f, distance); + // Head towards our target + var dir = (Target.transform.position - transform.position).normalized; + var deltaMove = dir * moveAmount * Time.fixedDeltaTime; + m_NetworkRigidbody.Rigidbody.MovePosition(m_NetworkRigidbody.Rigidbody.position + deltaMove); + + + Log($" Loc: {transform.position} | Dest: {Target.transform.position} | Dist: {distance} | MoveDelta: {deltaMove}"); + } + + protected void Log(string msg) + { + if (VerboseDebug) + { + Debug.Log($"Client-{OwnerClientId} {msg}"); + } + } + } + #endregion +} +#endif diff --git a/com.unity.netcode.gameobjects/Tests/Runtime/Physics/RigidbodyContactEventManagerTests.cs.meta b/com.unity.netcode.gameobjects/Tests/Runtime/Physics/RigidbodyContactEventManagerTests.cs.meta new file mode 100644 index 0000000000..687995a482 --- /dev/null +++ b/com.unity.netcode.gameobjects/Tests/Runtime/Physics/RigidbodyContactEventManagerTests.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 6d002e232abdb5049aa3c4f5cc36292d \ No newline at end of file diff --git a/testproject/Assets/Tests/Runtime/Physics.meta b/testproject/Assets/Tests/Runtime/Physics.meta deleted file mode 100644 index de0808f1a7..0000000000 --- a/testproject/Assets/Tests/Runtime/Physics.meta +++ /dev/null @@ -1,8 +0,0 @@ -fileFormatVersion: 2 -guid: efc90b4d4d76f7a48ad1cf726a3a193e -folderAsset: yes -DefaultImporter: - externalObjects: {} - userData: - assetBundleName: - assetBundleVariant: diff --git a/testproject/Assets/Tests/Runtime/Physics/NetworkRigidbody2DCntChangeOwnershipTest.cs b/testproject/Assets/Tests/Runtime/Physics/NetworkRigidbody2DCntChangeOwnershipTest.cs deleted file mode 100644 index 4f3c651cb3..0000000000 --- a/testproject/Assets/Tests/Runtime/Physics/NetworkRigidbody2DCntChangeOwnershipTest.cs +++ /dev/null @@ -1,95 +0,0 @@ -// using System.Collections; -// using NUnit.Framework; -// using Unity.Netcode.Components; -// using Unity.Netcode.Samples; -// using UnityEngine; -// using UnityEngine.TestTools; -// -// // Tests for ClientNetworkTransform (CNT) + NetworkRigidbody. This test is in TestProject because it needs access to ClientNetworkTransform -// namespace Unity.Netcode.RuntimeTests -// { -// public class NetworkRigidbody2DDynamicCntChangeOwnershipTest : NetworkRigidbody2DCntChangeOwnershipTestBase -// { -// public override bool Kinematic => false; -// } -// -// public class NetworkRigidbody2DKinematicCntChangeOwnershipTest : NetworkRigidbody2DCntChangeOwnershipTestBase -// { -// public override bool Kinematic => true; -// } -// -// public abstract class NetworkRigidbody2DCntChangeOwnershipTestBase : NetcodeIntegrationTest -// { -// protected override int NumberOfClients => 1; -// -// public abstract bool Kinematic { get; } -// -// [UnitySetUp] -// public override IEnumerator Setup() -// { -// yield return StartSomeClientsAndServerWithPlayers(true, NumberOfClients, playerPrefab => -// { -// playerPrefab.AddComponent(); -// playerPrefab.AddComponent(); -// playerPrefab.AddComponent(); -// playerPrefab.GetComponent().isKinematic = Kinematic; -// }); -// } -// /// -// /// Tests that a server can destroy a NetworkObject and that it gets despawned correctly. -// /// -// /// -// [UnityTest] -// public IEnumerator TestRigidbodyKinematicEnableDisable() -// { -// // This is the *SERVER VERSION* of the *CLIENT PLAYER* -// var serverClientPlayerResult = new NetcodeIntegrationTestHelpers.ResultWrapper(); -// yield return NetcodeIntegrationTestHelpers.GetNetworkObjectByRepresentation((x => x.IsPlayerObject && x.OwnerClientId == m_ClientNetworkManagers[0].LocalClientId), m_ServerNetworkManager, serverClientPlayerResult); -// var serverPlayer = serverClientPlayerResult.Result.gameObject; -// -// // This is the *CLIENT VERSION* of the *CLIENT PLAYER* -// var clientClientPlayerResult = new NetcodeIntegrationTestHelpers.ResultWrapper(); -// yield return NetcodeIntegrationTestHelpers.GetNetworkObjectByRepresentation((x => x.IsPlayerObject && x.OwnerClientId == m_ClientNetworkManagers[0].LocalClientId), m_ClientNetworkManagers[0], clientClientPlayerResult); -// var clientPlayer = clientClientPlayerResult.Result.gameObject; -// -// Assert.IsNotNull(serverPlayer); -// Assert.IsNotNull(clientPlayer); -// -// int waitFor = Time.frameCount + 2; -// yield return new WaitUntil(() => Time.frameCount >= waitFor); -// -// TestKinematicSetCorrectly(clientPlayer, serverPlayer); -// -// -// // give server ownership over the player -// -// serverPlayer.GetComponent().ChangeOwnership(NetworkManager.ServerClientId); -// -// yield return null; -// yield return null; -// -// // server should now be able to commit to transform -// TestKinematicSetCorrectly(serverPlayer, clientPlayer); -// -// // return ownership to client -// serverPlayer.GetComponent().ChangeOwnership(m_ClientNetworkManagers[0].LocalClientId); -// yield return null; -// yield return null; -// -// // client should again be able to commit -// TestKinematicSetCorrectly(clientPlayer, serverPlayer); -// } -// -// -// -// private void TestKinematicSetCorrectly(GameObject canCommitPlayer, GameObject canNotCommitPlayer) -// { -// -// // can commit player has authority and should have a kinematic mode of false (or true in case body was already kinematic). -// Assert.True(canCommitPlayer.GetComponent().isKinematic == Kinematic); -// -// // can not commit player has no authority and should have a kinematic mode of true -// Assert.True(canNotCommitPlayer.GetComponent().isKinematic); -// } -// } -// } diff --git a/testproject/Assets/Tests/Runtime/Physics/NetworkRigidbody2DCntChangeOwnershipTest.cs.meta b/testproject/Assets/Tests/Runtime/Physics/NetworkRigidbody2DCntChangeOwnershipTest.cs.meta deleted file mode 100644 index b1c3736256..0000000000 --- a/testproject/Assets/Tests/Runtime/Physics/NetworkRigidbody2DCntChangeOwnershipTest.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: a51c7f7b93f04e3499089963d9bbb254 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/testproject/Assets/Tests/Runtime/Physics/NetworkRigidbody2DCntTest.cs b/testproject/Assets/Tests/Runtime/Physics/NetworkRigidbody2DCntTest.cs deleted file mode 100644 index eac6d42d7e..0000000000 --- a/testproject/Assets/Tests/Runtime/Physics/NetworkRigidbody2DCntTest.cs +++ /dev/null @@ -1,82 +0,0 @@ -// using System.Collections; -// using NUnit.Framework; -// using Unity.Netcode.Components; -// using Unity.Netcode.Samples; -// using UnityEngine; -// using UnityEngine.TestTools; -// -// // Tests for ClientNetworkTransform (CNT) + NetworkRigidbody2D. This test is in TestProject because it needs access to ClientNetworkTransform -// namespace Unity.Netcode.RuntimeTests -// { -// public class NetworkRigidbody2DDynamicCntTest : NetworkRigidbody2DCntTestBase -// { -// public override bool Kinematic => false; -// } -// -// public class NetworkRigidbody2DKinematicCntTest : NetworkRigidbody2DCntTestBase -// { -// public override bool Kinematic => true; -// } -// -// public abstract class NetworkRigidbody2DCntTestBase : NetcodeIntegrationTest -// { -// protected override int NumberOfClients => 1; -// -// public abstract bool Kinematic { get; } -// -// [UnitySetUp] -// public override IEnumerator Setup() -// { -// yield return StartSomeClientsAndServerWithPlayers(true, NumberOfClients, playerPrefab => -// { -// playerPrefab.AddComponent(); -// playerPrefab.AddComponent(); -// playerPrefab.AddComponent(); -// playerPrefab.GetComponent().isKinematic = Kinematic; -// }); -// } -// -// /// -// /// Tests that a server can destroy a NetworkObject and that it gets despawned correctly. -// /// -// /// -// [UnityTest] -// public IEnumerator TestRigidbodyKinematicEnableDisable() -// { -// // This is the *SERVER VERSION* of the *CLIENT PLAYER* -// var serverClientPlayerResult = new NetcodeIntegrationTestHelpers.ResultWrapper(); -// yield return NetcodeIntegrationTestHelpers.GetNetworkObjectByRepresentation((x => x.IsPlayerObject && x.OwnerClientId == m_ClientNetworkManagers[0].LocalClientId), m_ServerNetworkManager, serverClientPlayerResult); -// var serverPlayer = serverClientPlayerResult.Result.gameObject; -// -// // This is the *CLIENT VERSION* of the *CLIENT PLAYER* -// var clientClientPlayerResult = new NetcodeIntegrationTestHelpers.ResultWrapper(); -// yield return NetcodeIntegrationTestHelpers.GetNetworkObjectByRepresentation((x => x.IsPlayerObject && x.OwnerClientId == m_ClientNetworkManagers[0].LocalClientId), m_ClientNetworkManagers[0], clientClientPlayerResult); -// var clientPlayer = clientClientPlayerResult.Result.gameObject; -// -// Assert.IsNotNull(serverPlayer); -// Assert.IsNotNull(clientPlayer); -// -// int waitFor = Time.frameCount + 2; -// yield return new WaitUntil(() => Time.frameCount >= waitFor); -// -// TestKinematicSetCorrectly(clientPlayer, serverPlayer); -// -// // despawn the server player -// serverPlayer.GetComponent().Despawn(false); -// -// yield return null; -// yield return null; -// Assert.IsTrue(clientPlayer == null); // safety check that object is actually despawned. -// } -// -// private void TestKinematicSetCorrectly(GameObject canCommitPlayer, GameObject canNotCommitPlayer) -// { -// -// // can commit player has authority and should have a kinematic mode of false (or true in case body was already kinematic). -// Assert.True(canCommitPlayer.GetComponent().isKinematic == Kinematic); -// -// // can not commit player has no authority and should have a kinematic mode of true -// Assert.True(canNotCommitPlayer.GetComponent().isKinematic); -// } -// } -// } diff --git a/testproject/Assets/Tests/Runtime/Physics/NetworkRigidbody2DCntTest.cs.meta b/testproject/Assets/Tests/Runtime/Physics/NetworkRigidbody2DCntTest.cs.meta deleted file mode 100644 index f0d4cff600..0000000000 --- a/testproject/Assets/Tests/Runtime/Physics/NetworkRigidbody2DCntTest.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: 5ae5587c06cba7a46b93ee6774741c0d -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/testproject/Assets/Tests/Runtime/Physics/NetworkRigidbodyCntChangeOwnershipTest.cs b/testproject/Assets/Tests/Runtime/Physics/NetworkRigidbodyCntChangeOwnershipTest.cs deleted file mode 100644 index 2e45162f52..0000000000 --- a/testproject/Assets/Tests/Runtime/Physics/NetworkRigidbodyCntChangeOwnershipTest.cs +++ /dev/null @@ -1,95 +0,0 @@ -// using System.Collections; -// using NUnit.Framework; -// using Unity.Netcode.Components; -// using Unity.Netcode.Samples; -// using UnityEngine; -// using UnityEngine.TestTools; -// -// // Tests for ClientNetworkTransform (CNT) + NetworkRigidbody. This test is in TestProject because it needs access to ClientNetworkTransform -// namespace Unity.Netcode.RuntimeTests -// { -// public class NetworkRigidbodyDynamicCntChangeOwnershipTest : NetworkRigidbodyCntChangeOwnershipTestBase -// { -// public override bool Kinematic => false; -// } -// -// public class NetworkRigidbodyKinematicCntChangeOwnershipTest : NetworkRigidbodyCntChangeOwnershipTestBase -// { -// public override bool Kinematic => true; -// } -// -// public abstract class NetworkRigidbodyCntChangeOwnershipTestBase : NetcodeIntegrationTest -// { -// protected override int NumberOfClients => 1; -// -// public abstract bool Kinematic { get; } -// -// [UnitySetUp] -// public override IEnumerator Setup() -// { -// yield return StartSomeClientsAndServerWithPlayers(true, NumberOfClients, playerPrefab => -// { -// playerPrefab.AddComponent(); -// playerPrefab.AddComponent(); -// playerPrefab.AddComponent(); -// playerPrefab.GetComponent().isKinematic = Kinematic; -// }); -// } -// /// -// /// Tests that a server can destroy a NetworkObject and that it gets despawned correctly. -// /// -// /// -// [UnityTest] -// public IEnumerator TestRigidbodyKinematicEnableDisable() -// { -// // This is the *SERVER VERSION* of the *CLIENT PLAYER* -// var serverClientPlayerResult = new NetcodeIntegrationTestHelpers.ResultWrapper(); -// yield return NetcodeIntegrationTestHelpers.GetNetworkObjectByRepresentation((x => x.IsPlayerObject && x.OwnerClientId == m_ClientNetworkManagers[0].LocalClientId), m_ServerNetworkManager, serverClientPlayerResult); -// var serverPlayer = serverClientPlayerResult.Result.gameObject; -// -// // This is the *CLIENT VERSION* of the *CLIENT PLAYER* -// var clientClientPlayerResult = new NetcodeIntegrationTestHelpers.ResultWrapper(); -// yield return NetcodeIntegrationTestHelpers.GetNetworkObjectByRepresentation((x => x.IsPlayerObject && x.OwnerClientId == m_ClientNetworkManagers[0].LocalClientId), m_ClientNetworkManagers[0], clientClientPlayerResult); -// var clientPlayer = clientClientPlayerResult.Result.gameObject; -// -// Assert.IsNotNull(serverPlayer); -// Assert.IsNotNull(clientPlayer); -// -// int waitFor = Time.frameCount + 2; -// yield return new WaitUntil(() => Time.frameCount >= waitFor); -// -// TestKinematicSetCorrectly(clientPlayer, serverPlayer); -// -// -// // give server ownership over the player -// -// serverPlayer.GetComponent().ChangeOwnership(NetworkManager.ServerClientId); -// -// yield return null; -// yield return null; -// -// // server should now be able to commit to transform -// TestKinematicSetCorrectly(serverPlayer, clientPlayer); -// -// // return ownership to client -// serverPlayer.GetComponent().ChangeOwnership(m_ClientNetworkManagers[0].LocalClientId); -// yield return null; -// yield return null; -// -// // client should again be able to commit -// TestKinematicSetCorrectly(clientPlayer, serverPlayer); -// } -// -// -// -// private void TestKinematicSetCorrectly(GameObject canCommitPlayer, GameObject canNotCommitPlayer) -// { -// -// // can commit player has authority and should have a kinematic mode of false (or true in case body was already kinematic). -// Assert.True(canCommitPlayer.GetComponent().isKinematic == Kinematic); -// -// // can not commit player has no authority and should have a kinematic mode of true -// Assert.True(canNotCommitPlayer.GetComponent().isKinematic); -// } -// } -// } diff --git a/testproject/Assets/Tests/Runtime/Physics/NetworkRigidbodyCntChangeOwnershipTest.cs.meta b/testproject/Assets/Tests/Runtime/Physics/NetworkRigidbodyCntChangeOwnershipTest.cs.meta deleted file mode 100644 index 3eb4f88f5b..0000000000 --- a/testproject/Assets/Tests/Runtime/Physics/NetworkRigidbodyCntChangeOwnershipTest.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: 941008c8040f03c44bd835d24b073260 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/testproject/Assets/Tests/Runtime/Physics/NetworkRigidbodyCntTest.cs b/testproject/Assets/Tests/Runtime/Physics/NetworkRigidbodyCntTest.cs deleted file mode 100644 index 72f0c72b96..0000000000 --- a/testproject/Assets/Tests/Runtime/Physics/NetworkRigidbodyCntTest.cs +++ /dev/null @@ -1,82 +0,0 @@ -// using System.Collections; -// using NUnit.Framework; -// using Unity.Netcode.Components; -// using Unity.Netcode.Samples; -// using UnityEngine; -// using UnityEngine.TestTools; -// -// // Tests for ClientNetworkTransform (CNT) + NetworkRigidbody. This test is in TestProject because it needs access to ClientNetworkTransform -// namespace Unity.Netcode.RuntimeTests -// { -// public class NetworkRigidbodyDynamicCntTest : NetworkRigidbodyCntTestBase -// { -// public override bool Kinematic => false; -// } -// -// public class NetworkRigidbodyKinematicCntTest : NetworkRigidbodyCntTestBase -// { -// public override bool Kinematic => true; -// } -// -// public abstract class NetworkRigidbodyCntTestBase : NetcodeIntegrationTest -// { -// protected override int NumberOfClients => 1; -// -// public abstract bool Kinematic { get; } -// -// [UnitySetUp] -// public override IEnumerator Setup() -// { -// yield return StartSomeClientsAndServerWithPlayers(true, NumberOfClients, playerPrefab => -// { -// playerPrefab.AddComponent(); -// playerPrefab.AddComponent(); -// playerPrefab.AddComponent(); -// playerPrefab.GetComponent().isKinematic = Kinematic; -// }); -// } -// /// -// /// Tests that a server can destroy a NetworkObject and that it gets despawned correctly. -// /// -// /// -// [UnityTest] -// public IEnumerator TestRigidbodyKinematicEnableDisable() -// { -// // This is the *SERVER VERSION* of the *CLIENT PLAYER* -// var serverClientPlayerResult = new NetcodeIntegrationTestHelpers.ResultWrapper(); -// yield returnNetcodeIntegrationTestHelpers.GetNetworkObjectByRepresentation((x => x.IsPlayerObject && x.OwnerClientId == m_ClientNetworkManagers[0].LocalClientId), m_ServerNetworkManager, serverClientPlayerResult); -// var serverPlayer = serverClientPlayerResult.Result.gameObject; -// -// // This is the *CLIENT VERSION* of the *CLIENT PLAYER* -// var clientClientPlayerResult = new NetcodeIntegrationTestHelpers.ResultWrapper(); -// yield return NetcodeIntegrationTestHelpers.GetNetworkObjectByRepresentation((x => x.IsPlayerObject && x.OwnerClientId == m_ClientNetworkManagers[0].LocalClientId), m_ClientNetworkManagers[0], clientClientPlayerResult); -// var clientPlayer = clientClientPlayerResult.Result.gameObject; -// -// Assert.IsNotNull(serverPlayer); -// Assert.IsNotNull(clientPlayer); -// -// int waitFor = Time.frameCount + 2; -// yield return new WaitUntil(() => Time.frameCount >= waitFor); -// -// TestKinematicSetCorrectly(clientPlayer, serverPlayer); -// -// // despawn the server player -// serverPlayer.GetComponent().Despawn(false); -// -// yield return null; -// yield return null; -// -// Assert.IsTrue(clientPlayer == null); // safety check that object is actually despawned. -// } -// -// private void TestKinematicSetCorrectly(GameObject canCommitPlayer, GameObject canNotCommitPlayer) -// { -// -// // can commit player has authority and should have a kinematic mode of false (or true in case body was already kinematic). -// Assert.True(canCommitPlayer.GetComponent().isKinematic == Kinematic); -// -// // can not commit player has no authority and should have a kinematic mode of true -// Assert.True(canNotCommitPlayer.GetComponent().isKinematic); -// } -// } -// } diff --git a/testproject/Assets/Tests/Runtime/Physics/NetworkRigidbodyCntTest.cs.meta b/testproject/Assets/Tests/Runtime/Physics/NetworkRigidbodyCntTest.cs.meta deleted file mode 100644 index cea97efc01..0000000000 --- a/testproject/Assets/Tests/Runtime/Physics/NetworkRigidbodyCntTest.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: e89712f9406aaa84d85508fa07d97655 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: From dcd8147281f2c7aabb7601e16fb9dcff9aa11c58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Chrobot?= <124174716+michalChrobot@users.noreply.github.com> Date: Wed, 24 Jun 2026 19:13:27 +0200 Subject: [PATCH 17/26] ci: CI deps update and changelog correction (#4034) * changelog correction * Wrench and deps update and CI regeneration * corrected editors * Addressed Standards check complains --- .yamato/project.metafile | 8 +- .yamato/wrench/api-validation-jobs.yml | 4 +- .yamato/wrench/package-pack-jobs.yml | 2 +- .yamato/wrench/preview-a-p-v.yml | 258 +++++++++++++--- .yamato/wrench/promotion-jobs.yml | 80 ++++- .yamato/wrench/recipe-regeneration.yml | 2 +- .yamato/wrench/validation-jobs.yml | 286 +++++++++++++++--- .yamato/wrench/wrench_config.json | 2 +- Tools/CI/NGO.Cookbook.csproj | 10 +- com.unity.netcode.gameobjects/CHANGELOG.md | 4 +- .../Editor/CodeGen/NetworkBehaviourILPP.cs | 16 +- .../Editor/NetworkManagerHelper.cs | 8 +- .../BufferedLinearInterpolator.cs | 44 +-- .../Runtime/Components/NetworkAnimator.cs | 58 ++-- .../Runtime/Components/NetworkTransform.cs | 38 +-- .../Connection/NetworkConnectionManager.cs | 20 +- .../Runtime/Core/NetworkObject.cs | 54 ++-- .../Messaging/Messages/CreateObjectMessage.cs | 8 +- .../DefaultSceneManagerHandler.cs | 10 +- .../Runtime/Spawning/NetworkPrefabHandler.cs | 8 +- .../Runtime/Spawning/NetworkSpawnManager.cs | 8 +- .../NetworkClientAndPlayerObjectTests.cs | 10 +- .../Tests/Runtime/NetworkShowHideTests.cs | 8 +- .../NetworkTransformNonAuthorityTests.cs | 12 +- .../Tests/Manual/Scripts/NetworkPrefabPool.cs | 10 +- .../Scripts/NetworkPrefabPoolAdditive.cs | 10 +- .../Runtime/Animation/NetworkAnimatorTests.cs | 8 +- 27 files changed, 720 insertions(+), 266 deletions(-) diff --git a/.yamato/project.metafile b/.yamato/project.metafile index f4802d6ff0..6db694d80c 100644 --- a/.yamato/project.metafile +++ b/.yamato/project.metafile @@ -24,7 +24,7 @@ small_agent_platform: - name: ubuntu type: Unity::VM - image: package-ci/ubuntu-22.04:v4.85.0 + image: package-ci/ubuntu-22.04:v4.86.0 flavor: b1.small @@ -39,13 +39,13 @@ test_platforms: default: - name: ubuntu type: Unity::VM - image: package-ci/ubuntu-22.04:v4.85.0 + image: package-ci/ubuntu-22.04:v4.86.0 flavor: b1.large standalone: StandaloneLinux64 desktop: - name: ubuntu type: Unity::VM - image: package-ci/ubuntu-22.04:v4.85.0 + image: package-ci/ubuntu-22.04:v4.86.0 flavor: b1.large smaller_flavor: b1.large larger_flavor: b1.xlarge @@ -178,8 +178,8 @@ validation_editors: all: - 6000.0 - 6000.3 - - 6000.4 - 6000.5 + - 6000.6 - trunk - f1298548e194f35ff7dfa6fee699d4464ab3919c minimal: diff --git a/.yamato/wrench/api-validation-jobs.yml b/.yamato/wrench/api-validation-jobs.yml index 6976c10973..1b9106d777 100644 --- a/.yamato/wrench/api-validation-jobs.yml +++ b/.yamato/wrench/api-validation-jobs.yml @@ -60,8 +60,8 @@ api_validation_-_netcode_gameobjects_-_6000_0_-_win10: UNITY_LICENSING_SERVER_DELETE_NUL: 0 UNITY_LICENSING_SERVER_DELETE_ULF: 0 UNITY_LICENSING_SERVER_TOOLSET: pro - UPMPVP_CONTEXT_WRENCH: 2.9.0.0 + UPMPVP_CONTEXT_WRENCH: 3.2.0.0 metadata: Job Maintainers: '#rm-packageworks' - Wrench: 2.9.0.0 + Wrench: 3.2.0.0 diff --git a/.yamato/wrench/package-pack-jobs.yml b/.yamato/wrench/package-pack-jobs.yml index 06c750a127..fec8ec8847 100644 --- a/.yamato/wrench/package-pack-jobs.yml +++ b/.yamato/wrench/package-pack-jobs.yml @@ -29,5 +29,5 @@ package_pack_-_netcode_gameobjects: UPMCI_ACK_LARGE_PACKAGE: 1 metadata: Job Maintainers: '#rm-packageworks' - Wrench: 2.9.0.0 + Wrench: 3.2.0.0 diff --git a/.yamato/wrench/preview-a-p-v.yml b/.yamato/wrench/preview-a-p-v.yml index 445f507a73..1029bfeb24 100644 --- a/.yamato/wrench/preview-a-p-v.yml +++ b/.yamato/wrench/preview-a-p-v.yml @@ -22,12 +22,15 @@ all_preview_apv_jobs: - path: .yamato/wrench/preview-a-p-v.yml#preview_apv_-_6000_5_-_macos13 - path: .yamato/wrench/preview-a-p-v.yml#preview_apv_-_6000_5_-_ubuntu2204 - path: .yamato/wrench/preview-a-p-v.yml#preview_apv_-_6000_5_-_win10 - - path: .yamato/wrench/preview-a-p-v.yml#preview_apv_-_6000_6_-_macos13 + - path: .yamato/wrench/preview-a-p-v.yml#preview_apv_-_6000_6_-_macos13arm - path: .yamato/wrench/preview-a-p-v.yml#preview_apv_-_6000_6_-_ubuntu2204 - path: .yamato/wrench/preview-a-p-v.yml#preview_apv_-_6000_6_-_win10 + - path: .yamato/wrench/preview-a-p-v.yml#preview_apv_-_6000_7_-_macos13arm + - path: .yamato/wrench/preview-a-p-v.yml#preview_apv_-_6000_7_-_ubuntu2204 + - path: .yamato/wrench/preview-a-p-v.yml#preview_apv_-_6000_7_-_win10 metadata: Job Maintainers: '#rm-packageworks' - Wrench: 2.9.0.0 + Wrench: 3.2.0.0 # Functional tests for dependents found in the latest 6000.0 manifest (MacOS). preview_apv_-_6000_0_-_macos13: @@ -82,10 +85,10 @@ preview_apv_-_6000_0_-_macos13: UNITY_LICENSING_SERVER_DELETE_NUL: 0 UNITY_LICENSING_SERVER_DELETE_ULF: 0 UNITY_LICENSING_SERVER_TOOLSET: pro - UPMPVP_CONTEXT_WRENCH: 2.9.0.0 + UPMPVP_CONTEXT_WRENCH: 3.2.0.0 metadata: Job Maintainers: '#rm-packageworks' - Wrench: 2.9.0.0 + Wrench: 3.2.0.0 # Functional tests for dependents found in the latest 6000.0 manifest (Ubuntu). preview_apv_-_6000_0_-_ubuntu2204: @@ -140,10 +143,10 @@ preview_apv_-_6000_0_-_ubuntu2204: UNITY_LICENSING_SERVER_DELETE_NUL: 0 UNITY_LICENSING_SERVER_DELETE_ULF: 0 UNITY_LICENSING_SERVER_TOOLSET: pro - UPMPVP_CONTEXT_WRENCH: 2.9.0.0 + UPMPVP_CONTEXT_WRENCH: 3.2.0.0 metadata: Job Maintainers: '#rm-packageworks' - Wrench: 2.9.0.0 + Wrench: 3.2.0.0 # Functional tests for dependents found in the latest 6000.0 manifest (Windows). preview_apv_-_6000_0_-_win10: @@ -199,10 +202,10 @@ preview_apv_-_6000_0_-_win10: UNITY_LICENSING_SERVER_DELETE_NUL: 0 UNITY_LICENSING_SERVER_DELETE_ULF: 0 UNITY_LICENSING_SERVER_TOOLSET: pro - UPMPVP_CONTEXT_WRENCH: 2.9.0.0 + UPMPVP_CONTEXT_WRENCH: 3.2.0.0 metadata: Job Maintainers: '#rm-packageworks' - Wrench: 2.9.0.0 + Wrench: 3.2.0.0 # Functional tests for dependents found in the latest 6000.3 manifest (MacOS). preview_apv_-_6000_3_-_macos13: @@ -257,10 +260,10 @@ preview_apv_-_6000_3_-_macos13: UNITY_LICENSING_SERVER_DELETE_NUL: 0 UNITY_LICENSING_SERVER_DELETE_ULF: 0 UNITY_LICENSING_SERVER_TOOLSET: pro - UPMPVP_CONTEXT_WRENCH: 2.9.0.0 + UPMPVP_CONTEXT_WRENCH: 3.2.0.0 metadata: Job Maintainers: '#rm-packageworks' - Wrench: 2.9.0.0 + Wrench: 3.2.0.0 # Functional tests for dependents found in the latest 6000.3 manifest (Ubuntu). preview_apv_-_6000_3_-_ubuntu2204: @@ -315,10 +318,10 @@ preview_apv_-_6000_3_-_ubuntu2204: UNITY_LICENSING_SERVER_DELETE_NUL: 0 UNITY_LICENSING_SERVER_DELETE_ULF: 0 UNITY_LICENSING_SERVER_TOOLSET: pro - UPMPVP_CONTEXT_WRENCH: 2.9.0.0 + UPMPVP_CONTEXT_WRENCH: 3.2.0.0 metadata: Job Maintainers: '#rm-packageworks' - Wrench: 2.9.0.0 + Wrench: 3.2.0.0 # Functional tests for dependents found in the latest 6000.3 manifest (Windows). preview_apv_-_6000_3_-_win10: @@ -374,10 +377,10 @@ preview_apv_-_6000_3_-_win10: UNITY_LICENSING_SERVER_DELETE_NUL: 0 UNITY_LICENSING_SERVER_DELETE_ULF: 0 UNITY_LICENSING_SERVER_TOOLSET: pro - UPMPVP_CONTEXT_WRENCH: 2.9.0.0 + UPMPVP_CONTEXT_WRENCH: 3.2.0.0 metadata: Job Maintainers: '#rm-packageworks' - Wrench: 2.9.0.0 + Wrench: 3.2.0.0 # Functional tests for dependents found in the latest 6000.4 manifest (MacOS). preview_apv_-_6000_4_-_macos13: @@ -432,10 +435,10 @@ preview_apv_-_6000_4_-_macos13: UNITY_LICENSING_SERVER_DELETE_NUL: 0 UNITY_LICENSING_SERVER_DELETE_ULF: 0 UNITY_LICENSING_SERVER_TOOLSET: pro - UPMPVP_CONTEXT_WRENCH: 2.9.0.0 + UPMPVP_CONTEXT_WRENCH: 3.2.0.0 metadata: Job Maintainers: '#rm-packageworks' - Wrench: 2.9.0.0 + Wrench: 3.2.0.0 # Functional tests for dependents found in the latest 6000.4 manifest (Ubuntu). preview_apv_-_6000_4_-_ubuntu2204: @@ -490,10 +493,10 @@ preview_apv_-_6000_4_-_ubuntu2204: UNITY_LICENSING_SERVER_DELETE_NUL: 0 UNITY_LICENSING_SERVER_DELETE_ULF: 0 UNITY_LICENSING_SERVER_TOOLSET: pro - UPMPVP_CONTEXT_WRENCH: 2.9.0.0 + UPMPVP_CONTEXT_WRENCH: 3.2.0.0 metadata: Job Maintainers: '#rm-packageworks' - Wrench: 2.9.0.0 + Wrench: 3.2.0.0 # Functional tests for dependents found in the latest 6000.4 manifest (Windows). preview_apv_-_6000_4_-_win10: @@ -549,10 +552,10 @@ preview_apv_-_6000_4_-_win10: UNITY_LICENSING_SERVER_DELETE_NUL: 0 UNITY_LICENSING_SERVER_DELETE_ULF: 0 UNITY_LICENSING_SERVER_TOOLSET: pro - UPMPVP_CONTEXT_WRENCH: 2.9.0.0 + UPMPVP_CONTEXT_WRENCH: 3.2.0.0 metadata: Job Maintainers: '#rm-packageworks' - Wrench: 2.9.0.0 + Wrench: 3.2.0.0 # Functional tests for dependents found in the latest 6000.5 manifest (MacOS). preview_apv_-_6000_5_-_macos13: @@ -607,10 +610,10 @@ preview_apv_-_6000_5_-_macos13: UNITY_LICENSING_SERVER_DELETE_NUL: 0 UNITY_LICENSING_SERVER_DELETE_ULF: 0 UNITY_LICENSING_SERVER_TOOLSET: pro - UPMPVP_CONTEXT_WRENCH: 2.9.0.0 + UPMPVP_CONTEXT_WRENCH: 3.2.0.0 metadata: Job Maintainers: '#rm-packageworks' - Wrench: 2.9.0.0 + Wrench: 3.2.0.0 # Functional tests for dependents found in the latest 6000.5 manifest (Ubuntu). preview_apv_-_6000_5_-_ubuntu2204: @@ -665,10 +668,10 @@ preview_apv_-_6000_5_-_ubuntu2204: UNITY_LICENSING_SERVER_DELETE_NUL: 0 UNITY_LICENSING_SERVER_DELETE_ULF: 0 UNITY_LICENSING_SERVER_TOOLSET: pro - UPMPVP_CONTEXT_WRENCH: 2.9.0.0 + UPMPVP_CONTEXT_WRENCH: 3.2.0.0 metadata: Job Maintainers: '#rm-packageworks' - Wrench: 2.9.0.0 + Wrench: 3.2.0.0 # Functional tests for dependents found in the latest 6000.5 manifest (Windows). preview_apv_-_6000_5_-_win10: @@ -724,18 +727,19 @@ preview_apv_-_6000_5_-_win10: UNITY_LICENSING_SERVER_DELETE_NUL: 0 UNITY_LICENSING_SERVER_DELETE_ULF: 0 UNITY_LICENSING_SERVER_TOOLSET: pro - UPMPVP_CONTEXT_WRENCH: 2.9.0.0 + UPMPVP_CONTEXT_WRENCH: 3.2.0.0 metadata: Job Maintainers: '#rm-packageworks' - Wrench: 2.9.0.0 + Wrench: 3.2.0.0 # Functional tests for dependents found in the latest 6000.6 manifest (MacOS). -preview_apv_-_6000_6_-_macos13: - name: Preview APV - 6000.6 - macos13 +preview_apv_-_6000_6_-_macos13arm: + name: Preview APV - 6000.6 - macos13arm agent: - image: package-ci/macos-13:v4 + image: package-ci/macos-13-arm64:v4 type: Unity::VM::osx - flavor: b1.xlarge + flavor: m1.mac + model: M1 commands: - command: curl https://artifactory.prd.it.unity3d.com/artifactory/stevedore-unity-internal/wrench-localapv/1-3-3_51b4e6affb4c10b90f92db9551b9dc80c5520794983600cff4fa925111db116d.zip -o wrench-localapv.zip - command: 7z x -aoa wrench-localapv.zip @@ -744,7 +748,7 @@ preview_apv_-_6000_6_-_macos13: - command: npm install upm-ci-utils@stable -g --registry https://artifactory.prd.cds.internal.unity3d.com/artifactory/api/npm/upm-npm timeout: 20 retries: 10 - - command: unity-downloader-cli -u trunk -c editor --path .Editor --fast + - command: unity-downloader-cli -u 6000.6/staging -c editor --path .Editor --fast --arch arm64 timeout: 10 retries: 3 - command: python PythonScripts/preview_apv.py --wrench-config=.yamato/wrench/wrench_config.json --editor-version=6000.6 --testsuite=editor,playmode --artifacts-path=PreviewApvArtifacts~ @@ -782,10 +786,10 @@ preview_apv_-_6000_6_-_macos13: UNITY_LICENSING_SERVER_DELETE_NUL: 0 UNITY_LICENSING_SERVER_DELETE_ULF: 0 UNITY_LICENSING_SERVER_TOOLSET: pro - UPMPVP_CONTEXT_WRENCH: 2.9.0.0 + UPMPVP_CONTEXT_WRENCH: 3.2.0.0 metadata: Job Maintainers: '#rm-packageworks' - Wrench: 2.9.0.0 + Wrench: 3.2.0.0 # Functional tests for dependents found in the latest 6000.6 manifest (Ubuntu). preview_apv_-_6000_6_-_ubuntu2204: @@ -802,7 +806,7 @@ preview_apv_-_6000_6_-_ubuntu2204: - command: npm install upm-ci-utils@stable -g --registry https://artifactory.prd.cds.internal.unity3d.com/artifactory/api/npm/upm-npm timeout: 20 retries: 10 - - command: unity-downloader-cli -u trunk -c editor --path .Editor --fast + - command: unity-downloader-cli -u 6000.6/staging -c editor --path .Editor --fast timeout: 10 retries: 3 - command: python PythonScripts/preview_apv.py --wrench-config=.yamato/wrench/wrench_config.json --editor-version=6000.6 --testsuite=editor,playmode --artifacts-path=PreviewApvArtifacts~ @@ -840,10 +844,10 @@ preview_apv_-_6000_6_-_ubuntu2204: UNITY_LICENSING_SERVER_DELETE_NUL: 0 UNITY_LICENSING_SERVER_DELETE_ULF: 0 UNITY_LICENSING_SERVER_TOOLSET: pro - UPMPVP_CONTEXT_WRENCH: 2.9.0.0 + UPMPVP_CONTEXT_WRENCH: 3.2.0.0 metadata: Job Maintainers: '#rm-packageworks' - Wrench: 2.9.0.0 + Wrench: 3.2.0.0 # Functional tests for dependents found in the latest 6000.6 manifest (Windows). preview_apv_-_6000_6_-_win10: @@ -861,7 +865,7 @@ preview_apv_-_6000_6_-_win10: - command: npm install upm-ci-utils@stable -g --registry https://artifactory.prd.cds.internal.unity3d.com/artifactory/api/npm/upm-npm timeout: 20 retries: 10 - - command: unity-downloader-cli -u trunk -c editor --path .Editor --fast + - command: unity-downloader-cli -u 6000.6/staging -c editor --path .Editor --fast timeout: 10 retries: 3 - command: python PythonScripts/preview_apv.py --wrench-config=.yamato/wrench/wrench_config.json --editor-version=6000.6 --testsuite=editor,playmode --artifacts-path=PreviewApvArtifacts~ @@ -899,8 +903,184 @@ preview_apv_-_6000_6_-_win10: UNITY_LICENSING_SERVER_DELETE_NUL: 0 UNITY_LICENSING_SERVER_DELETE_ULF: 0 UNITY_LICENSING_SERVER_TOOLSET: pro - UPMPVP_CONTEXT_WRENCH: 2.9.0.0 + UPMPVP_CONTEXT_WRENCH: 3.2.0.0 + metadata: + Job Maintainers: '#rm-packageworks' + Wrench: 3.2.0.0 + +# Functional tests for dependents found in the latest 6000.7 manifest (MacOS). +preview_apv_-_6000_7_-_macos13arm: + name: Preview APV - 6000.7 - macos13arm + agent: + image: package-ci/macos-13-arm64:v4 + type: Unity::VM::osx + flavor: m1.mac + model: M1 + commands: + - command: curl https://artifactory.prd.it.unity3d.com/artifactory/stevedore-unity-internal/wrench-localapv/1-3-3_51b4e6affb4c10b90f92db9551b9dc80c5520794983600cff4fa925111db116d.zip -o wrench-localapv.zip + - command: 7z x -aoa wrench-localapv.zip + - command: pip install semver requests --index-url https://artifactory-slo.bf.unity3d.com/artifactory/api/pypi/pypi/simple + - command: python PythonScripts/print_machine_info.py + - command: npm install upm-ci-utils@stable -g --registry https://artifactory.prd.cds.internal.unity3d.com/artifactory/api/npm/upm-npm + timeout: 20 + retries: 10 + - command: unity-downloader-cli -u trunk -c editor --path .Editor --fast --arch arm64 + timeout: 10 + retries: 3 + - command: python PythonScripts/preview_apv.py --wrench-config=.yamato/wrench/wrench_config.json --editor-version=6000.7 --testsuite=editor,playmode --artifacts-path=PreviewApvArtifacts~ + - command: echo 'Skipping Editor Manifest Validator as it is only supported on Windows' + after: + - command: bash .yamato/generated-scripts/infrastructure-instability-detection-mac.sh + artifacts: + Crash Dumps: + paths: + - CrashDumps/** + logs: + paths: + - '*.log' + - '*.xml' + - upm-ci~/test-results/**/* + - upm-ci~/temp/*/Logs/** + - upm-ci~/temp/*/Library/*.log + - upm-ci~/temp/*/*.log + - upm-ci~/temp/Builds/*.log + packages: + paths: + - upm-ci~/packages/**/* + PreviewAPVResults: + paths: + - PreviewApvArtifacts~/** + - APVTest/**/manifest.json + pvp-results: + paths: + - upm-ci~/pvp/**/* + browsable: onDemand + dependencies: + - path: .yamato/wrench/package-pack-jobs.yml#package_pack_-_netcode_gameobjects + variables: + UNITY_LICENSING_SERVER_BASE_URL: http://unity-ci-licenses.hq.unity3d.com:8080/ + UNITY_LICENSING_SERVER_DELETE_NUL: 0 + UNITY_LICENSING_SERVER_DELETE_ULF: 0 + UNITY_LICENSING_SERVER_TOOLSET: pro + UPMPVP_CONTEXT_WRENCH: 3.2.0.0 + metadata: + Job Maintainers: '#rm-packageworks' + Wrench: 3.2.0.0 + +# Functional tests for dependents found in the latest 6000.7 manifest (Ubuntu). +preview_apv_-_6000_7_-_ubuntu2204: + name: Preview APV - 6000.7 - ubuntu2204 + agent: + image: package-ci/ubuntu-22.04:v4 + type: Unity::VM + flavor: b1.large + commands: + - command: curl https://artifactory.prd.it.unity3d.com/artifactory/stevedore-unity-internal/wrench-localapv/1-3-3_51b4e6affb4c10b90f92db9551b9dc80c5520794983600cff4fa925111db116d.zip -o wrench-localapv.zip + - command: 7z x -aoa wrench-localapv.zip + - command: pip install semver requests --index-url https://artifactory-slo.bf.unity3d.com/artifactory/api/pypi/pypi/simple + - command: python PythonScripts/print_machine_info.py + - command: npm install upm-ci-utils@stable -g --registry https://artifactory.prd.cds.internal.unity3d.com/artifactory/api/npm/upm-npm + timeout: 20 + retries: 10 + - command: unity-downloader-cli -u trunk -c editor --path .Editor --fast + timeout: 10 + retries: 3 + - command: python PythonScripts/preview_apv.py --wrench-config=.yamato/wrench/wrench_config.json --editor-version=6000.7 --testsuite=editor,playmode --artifacts-path=PreviewApvArtifacts~ + - command: echo 'Skipping Editor Manifest Validator as it is only supported on Windows' + after: + - command: bash .yamato/generated-scripts/infrastructure-instability-detection-linux.sh + artifacts: + Crash Dumps: + paths: + - CrashDumps/** + logs: + paths: + - '*.log' + - '*.xml' + - upm-ci~/test-results/**/* + - upm-ci~/temp/*/Logs/** + - upm-ci~/temp/*/Library/*.log + - upm-ci~/temp/*/*.log + - upm-ci~/temp/Builds/*.log + packages: + paths: + - upm-ci~/packages/**/* + PreviewAPVResults: + paths: + - PreviewApvArtifacts~/** + - APVTest/**/manifest.json + pvp-results: + paths: + - upm-ci~/pvp/**/* + browsable: onDemand + dependencies: + - path: .yamato/wrench/package-pack-jobs.yml#package_pack_-_netcode_gameobjects + variables: + UNITY_LICENSING_SERVER_BASE_URL: http://unity-ci-licenses.hq.unity3d.com:8080/ + UNITY_LICENSING_SERVER_DELETE_NUL: 0 + UNITY_LICENSING_SERVER_DELETE_ULF: 0 + UNITY_LICENSING_SERVER_TOOLSET: pro + UPMPVP_CONTEXT_WRENCH: 3.2.0.0 + metadata: + Job Maintainers: '#rm-packageworks' + Wrench: 3.2.0.0 + +# Functional tests for dependents found in the latest 6000.7 manifest (Windows). +preview_apv_-_6000_7_-_win10: + name: Preview APV - 6000.7 - win10 + agent: + image: package-ci/win10:v4 + type: Unity::VM + flavor: b1.large + commands: + - command: gsudo reg add "HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\FileSystem" /v LongPathsEnabled /t REG_DWORD /d 1 /f + - command: curl https://artifactory.prd.it.unity3d.com/artifactory/stevedore-unity-internal/wrench-localapv/1-3-3_51b4e6affb4c10b90f92db9551b9dc80c5520794983600cff4fa925111db116d.zip -o wrench-localapv.zip + - command: 7z x -aoa wrench-localapv.zip + - command: pip install semver requests --index-url https://artifactory-slo.bf.unity3d.com/artifactory/api/pypi/pypi/simple + - command: python PythonScripts/print_machine_info.py + - command: npm install upm-ci-utils@stable -g --registry https://artifactory.prd.cds.internal.unity3d.com/artifactory/api/npm/upm-npm + timeout: 20 + retries: 10 + - command: unity-downloader-cli -u trunk -c editor --path .Editor --fast + timeout: 10 + retries: 3 + - command: python PythonScripts/preview_apv.py --wrench-config=.yamato/wrench/wrench_config.json --editor-version=6000.7 --testsuite=editor,playmode --artifacts-path=PreviewApvArtifacts~ + - command: python PythonScripts/editor_manifest_validator.py --version=6000.7 --wrench-config=.yamato/wrench/wrench_config.json + after: + - command: .yamato\generated-scripts\infrastructure-instability-detection-win.cmd + artifacts: + Crash Dumps: + paths: + - CrashDumps/** + logs: + paths: + - '*.log' + - '*.xml' + - upm-ci~/test-results/**/* + - upm-ci~/temp/*/Logs/** + - upm-ci~/temp/*/Library/*.log + - upm-ci~/temp/*/*.log + - upm-ci~/temp/Builds/*.log + packages: + paths: + - upm-ci~/packages/**/* + PreviewAPVResults: + paths: + - PreviewApvArtifacts~/** + - APVTest/**/manifest.json + pvp-results: + paths: + - upm-ci~/pvp/**/* + browsable: onDemand + dependencies: + - path: .yamato/wrench/package-pack-jobs.yml#package_pack_-_netcode_gameobjects + variables: + UNITY_LICENSING_SERVER_BASE_URL: http://unity-ci-licenses.hq.unity3d.com:8080/ + UNITY_LICENSING_SERVER_DELETE_NUL: 0 + UNITY_LICENSING_SERVER_DELETE_ULF: 0 + UNITY_LICENSING_SERVER_TOOLSET: pro + UPMPVP_CONTEXT_WRENCH: 3.2.0.0 metadata: Job Maintainers: '#rm-packageworks' - Wrench: 2.9.0.0 + Wrench: 3.2.0.0 diff --git a/.yamato/wrench/promotion-jobs.yml b/.yamato/wrench/promotion-jobs.yml index 7fd22a018d..929cce45a2 100644 --- a/.yamato/wrench/promotion-jobs.yml +++ b/.yamato/wrench/promotion-jobs.yml @@ -151,15 +151,15 @@ publish_dry_run_netcode_gameobjects: UTR: location: results/UTR/validate-netcode.gameobjects-6000.5-win10 unzip: true - - path: .yamato/wrench/validation-jobs.yml#validate_-_netcode_gameobjects_-_6000_6_-_macos13 + - path: .yamato/wrench/validation-jobs.yml#validate_-_netcode_gameobjects_-_6000_6_-_macos13arm specific_options: packages: ignore_artifact: true pvp-results: - location: results/pvp/validate-netcode.gameobjects-6000.6-macos13 + location: results/pvp/validate-netcode.gameobjects-6000.6-macos13arm unzip: true UTR: - location: results/UTR/validate-netcode.gameobjects-6000.6-macos13 + location: results/UTR/validate-netcode.gameobjects-6000.6-macos13arm unzip: true - path: .yamato/wrench/validation-jobs.yml#validate_-_netcode_gameobjects_-_6000_6_-_ubuntu2204 specific_options: @@ -181,12 +181,42 @@ publish_dry_run_netcode_gameobjects: UTR: location: results/UTR/validate-netcode.gameobjects-6000.6-win10 unzip: true + - path: .yamato/wrench/validation-jobs.yml#validate_-_netcode_gameobjects_-_6000_7_-_macos13arm + specific_options: + packages: + ignore_artifact: true + pvp-results: + location: results/pvp/validate-netcode.gameobjects-6000.7-macos13arm + unzip: true + UTR: + location: results/UTR/validate-netcode.gameobjects-6000.7-macos13arm + unzip: true + - path: .yamato/wrench/validation-jobs.yml#validate_-_netcode_gameobjects_-_6000_7_-_ubuntu2204 + specific_options: + packages: + ignore_artifact: true + pvp-results: + location: results/pvp/validate-netcode.gameobjects-6000.7-ubuntu2204 + unzip: true + UTR: + location: results/UTR/validate-netcode.gameobjects-6000.7-ubuntu2204 + unzip: true + - path: .yamato/wrench/validation-jobs.yml#validate_-_netcode_gameobjects_-_6000_7_-_win10 + specific_options: + packages: + ignore_artifact: true + pvp-results: + location: results/pvp/validate-netcode.gameobjects-6000.7-win10 + unzip: true + UTR: + location: results/UTR/validate-netcode.gameobjects-6000.7-win10 + unzip: true variables: UPMPVP_ACK_UPMPVP_DOES_NO_API_VALIDATION: 1 - UPMPVP_CONTEXT_WRENCH: 2.9.0.0 + UPMPVP_CONTEXT_WRENCH: 3.2.0.0 metadata: Job Maintainers: '#rm-packageworks' - Wrench: 2.9.0.0 + Wrench: 3.2.0.0 # Publish for netcode.gameobjects to https://artifactory-slo.bf.unity3d.com/artifactory/api/npm/upm-npm publish_netcode_gameobjects: @@ -333,15 +363,15 @@ publish_netcode_gameobjects: UTR: location: results/UTR/validate-netcode.gameobjects-6000.5-win10 unzip: true - - path: .yamato/wrench/validation-jobs.yml#validate_-_netcode_gameobjects_-_6000_6_-_macos13 + - path: .yamato/wrench/validation-jobs.yml#validate_-_netcode_gameobjects_-_6000_6_-_macos13arm specific_options: packages: ignore_artifact: true pvp-results: - location: results/pvp/validate-netcode.gameobjects-6000.6-macos13 + location: results/pvp/validate-netcode.gameobjects-6000.6-macos13arm unzip: true UTR: - location: results/UTR/validate-netcode.gameobjects-6000.6-macos13 + location: results/UTR/validate-netcode.gameobjects-6000.6-macos13arm unzip: true - path: .yamato/wrench/validation-jobs.yml#validate_-_netcode_gameobjects_-_6000_6_-_ubuntu2204 specific_options: @@ -363,11 +393,41 @@ publish_netcode_gameobjects: UTR: location: results/UTR/validate-netcode.gameobjects-6000.6-win10 unzip: true + - path: .yamato/wrench/validation-jobs.yml#validate_-_netcode_gameobjects_-_6000_7_-_macos13arm + specific_options: + packages: + ignore_artifact: true + pvp-results: + location: results/pvp/validate-netcode.gameobjects-6000.7-macos13arm + unzip: true + UTR: + location: results/UTR/validate-netcode.gameobjects-6000.7-macos13arm + unzip: true + - path: .yamato/wrench/validation-jobs.yml#validate_-_netcode_gameobjects_-_6000_7_-_ubuntu2204 + specific_options: + packages: + ignore_artifact: true + pvp-results: + location: results/pvp/validate-netcode.gameobjects-6000.7-ubuntu2204 + unzip: true + UTR: + location: results/UTR/validate-netcode.gameobjects-6000.7-ubuntu2204 + unzip: true + - path: .yamato/wrench/validation-jobs.yml#validate_-_netcode_gameobjects_-_6000_7_-_win10 + specific_options: + packages: + ignore_artifact: true + pvp-results: + location: results/pvp/validate-netcode.gameobjects-6000.7-win10 + unzip: true + UTR: + location: results/UTR/validate-netcode.gameobjects-6000.7-win10 + unzip: true variables: UPMPVP_ACK_UPMPVP_DOES_NO_API_VALIDATION: 1 - UPMPVP_CONTEXT_WRENCH: 2.9.0.0 + UPMPVP_CONTEXT_WRENCH: 3.2.0.0 allow_on: branch match "^release/.*" metadata: Job Maintainers: '#rm-packageworks' - Wrench: 2.9.0.0 + Wrench: 3.2.0.0 diff --git a/.yamato/wrench/recipe-regeneration.yml b/.yamato/wrench/recipe-regeneration.yml index b9eba06e54..34d5f02642 100644 --- a/.yamato/wrench/recipe-regeneration.yml +++ b/.yamato/wrench/recipe-regeneration.yml @@ -31,5 +31,5 @@ test_-_wrench_jobs_up_to_date: cancel_old_ci: true metadata: Job Maintainers: '#rm-packageworks' - Wrench: 2.9.0.0 + Wrench: 3.2.0.0 diff --git a/.yamato/wrench/validation-jobs.yml b/.yamato/wrench/validation-jobs.yml index 84a92fcd75..9e05a163af 100644 --- a/.yamato/wrench/validation-jobs.yml +++ b/.yamato/wrench/validation-jobs.yml @@ -69,10 +69,10 @@ validate_-_netcode_gameobjects_-_6000_0_-_macos13: UNITY_LICENSING_SERVER_DELETE_ULF: 0 UNITY_LICENSING_SERVER_TOOLSET: pro UPMPVP_ACK_UPMPVP_DOES_NO_API_VALIDATION: 1 - UPMPVP_CONTEXT_WRENCH: 2.9.0.0 + UPMPVP_CONTEXT_WRENCH: 3.2.0.0 metadata: Job Maintainers: '#rm-packageworks' - Wrench: 2.9.0.0 + Wrench: 3.2.0.0 labels: - Packages:netcode.gameobjects @@ -139,10 +139,10 @@ validate_-_netcode_gameobjects_-_6000_0_-_ubuntu2204: UNITY_LICENSING_SERVER_DELETE_ULF: 0 UNITY_LICENSING_SERVER_TOOLSET: pro UPMPVP_ACK_UPMPVP_DOES_NO_API_VALIDATION: 1 - UPMPVP_CONTEXT_WRENCH: 2.9.0.0 + UPMPVP_CONTEXT_WRENCH: 3.2.0.0 metadata: Job Maintainers: '#rm-packageworks' - Wrench: 2.9.0.0 + Wrench: 3.2.0.0 labels: - Packages:netcode.gameobjects @@ -209,10 +209,10 @@ validate_-_netcode_gameobjects_-_6000_0_-_win10: UNITY_LICENSING_SERVER_DELETE_ULF: 0 UNITY_LICENSING_SERVER_TOOLSET: pro UPMPVP_ACK_UPMPVP_DOES_NO_API_VALIDATION: 1 - UPMPVP_CONTEXT_WRENCH: 2.9.0.0 + UPMPVP_CONTEXT_WRENCH: 3.2.0.0 metadata: Job Maintainers: '#rm-packageworks' - Wrench: 2.9.0.0 + Wrench: 3.2.0.0 labels: - Packages:netcode.gameobjects @@ -279,10 +279,10 @@ validate_-_netcode_gameobjects_-_6000_3_-_macos13: UNITY_LICENSING_SERVER_DELETE_ULF: 0 UNITY_LICENSING_SERVER_TOOLSET: pro UPMPVP_ACK_UPMPVP_DOES_NO_API_VALIDATION: 1 - UPMPVP_CONTEXT_WRENCH: 2.9.0.0 + UPMPVP_CONTEXT_WRENCH: 3.2.0.0 metadata: Job Maintainers: '#rm-packageworks' - Wrench: 2.9.0.0 + Wrench: 3.2.0.0 labels: - Packages:netcode.gameobjects @@ -349,10 +349,10 @@ validate_-_netcode_gameobjects_-_6000_3_-_ubuntu2204: UNITY_LICENSING_SERVER_DELETE_ULF: 0 UNITY_LICENSING_SERVER_TOOLSET: pro UPMPVP_ACK_UPMPVP_DOES_NO_API_VALIDATION: 1 - UPMPVP_CONTEXT_WRENCH: 2.9.0.0 + UPMPVP_CONTEXT_WRENCH: 3.2.0.0 metadata: Job Maintainers: '#rm-packageworks' - Wrench: 2.9.0.0 + Wrench: 3.2.0.0 labels: - Packages:netcode.gameobjects @@ -419,10 +419,10 @@ validate_-_netcode_gameobjects_-_6000_3_-_win10: UNITY_LICENSING_SERVER_DELETE_ULF: 0 UNITY_LICENSING_SERVER_TOOLSET: pro UPMPVP_ACK_UPMPVP_DOES_NO_API_VALIDATION: 1 - UPMPVP_CONTEXT_WRENCH: 2.9.0.0 + UPMPVP_CONTEXT_WRENCH: 3.2.0.0 metadata: Job Maintainers: '#rm-packageworks' - Wrench: 2.9.0.0 + Wrench: 3.2.0.0 labels: - Packages:netcode.gameobjects @@ -489,10 +489,10 @@ validate_-_netcode_gameobjects_-_6000_4_-_macos13: UNITY_LICENSING_SERVER_DELETE_ULF: 0 UNITY_LICENSING_SERVER_TOOLSET: pro UPMPVP_ACK_UPMPVP_DOES_NO_API_VALIDATION: 1 - UPMPVP_CONTEXT_WRENCH: 2.9.0.0 + UPMPVP_CONTEXT_WRENCH: 3.2.0.0 metadata: Job Maintainers: '#rm-packageworks' - Wrench: 2.9.0.0 + Wrench: 3.2.0.0 labels: - Packages:netcode.gameobjects @@ -559,10 +559,10 @@ validate_-_netcode_gameobjects_-_6000_4_-_ubuntu2204: UNITY_LICENSING_SERVER_DELETE_ULF: 0 UNITY_LICENSING_SERVER_TOOLSET: pro UPMPVP_ACK_UPMPVP_DOES_NO_API_VALIDATION: 1 - UPMPVP_CONTEXT_WRENCH: 2.9.0.0 + UPMPVP_CONTEXT_WRENCH: 3.2.0.0 metadata: Job Maintainers: '#rm-packageworks' - Wrench: 2.9.0.0 + Wrench: 3.2.0.0 labels: - Packages:netcode.gameobjects @@ -629,10 +629,10 @@ validate_-_netcode_gameobjects_-_6000_4_-_win10: UNITY_LICENSING_SERVER_DELETE_ULF: 0 UNITY_LICENSING_SERVER_TOOLSET: pro UPMPVP_ACK_UPMPVP_DOES_NO_API_VALIDATION: 1 - UPMPVP_CONTEXT_WRENCH: 2.9.0.0 + UPMPVP_CONTEXT_WRENCH: 3.2.0.0 metadata: Job Maintainers: '#rm-packageworks' - Wrench: 2.9.0.0 + Wrench: 3.2.0.0 labels: - Packages:netcode.gameobjects @@ -699,10 +699,10 @@ validate_-_netcode_gameobjects_-_6000_5_-_macos13: UNITY_LICENSING_SERVER_DELETE_ULF: 0 UNITY_LICENSING_SERVER_TOOLSET: pro UPMPVP_ACK_UPMPVP_DOES_NO_API_VALIDATION: 1 - UPMPVP_CONTEXT_WRENCH: 2.9.0.0 + UPMPVP_CONTEXT_WRENCH: 3.2.0.0 metadata: Job Maintainers: '#rm-packageworks' - Wrench: 2.9.0.0 + Wrench: 3.2.0.0 labels: - Packages:netcode.gameobjects @@ -769,10 +769,10 @@ validate_-_netcode_gameobjects_-_6000_5_-_ubuntu2204: UNITY_LICENSING_SERVER_DELETE_ULF: 0 UNITY_LICENSING_SERVER_TOOLSET: pro UPMPVP_ACK_UPMPVP_DOES_NO_API_VALIDATION: 1 - UPMPVP_CONTEXT_WRENCH: 2.9.0.0 + UPMPVP_CONTEXT_WRENCH: 3.2.0.0 metadata: Job Maintainers: '#rm-packageworks' - Wrench: 2.9.0.0 + Wrench: 3.2.0.0 labels: - Packages:netcode.gameobjects @@ -839,26 +839,27 @@ validate_-_netcode_gameobjects_-_6000_5_-_win10: UNITY_LICENSING_SERVER_DELETE_ULF: 0 UNITY_LICENSING_SERVER_TOOLSET: pro UPMPVP_ACK_UPMPVP_DOES_NO_API_VALIDATION: 1 - UPMPVP_CONTEXT_WRENCH: 2.9.0.0 + UPMPVP_CONTEXT_WRENCH: 3.2.0.0 metadata: Job Maintainers: '#rm-packageworks' - Wrench: 2.9.0.0 + Wrench: 3.2.0.0 labels: - Packages:netcode.gameobjects -# PVP Editor and Playmode tests for Validate - netcode.gameobjects - 6000.6 - macos13 (6000.6 - MacOS). -validate_-_netcode_gameobjects_-_6000_6_-_macos13: - name: Validate - netcode.gameobjects - 6000.6 - macos13 +# PVP Editor and Playmode tests for Validate - netcode.gameobjects - 6000.6 - macos13arm (6000.6 - MacOS). +validate_-_netcode_gameobjects_-_6000_6_-_macos13arm: + name: Validate - netcode.gameobjects - 6000.6 - macos13arm agent: - image: package-ci/macos-13:v4 + image: package-ci/macos-13-arm64:v4 type: Unity::VM::osx - flavor: b1.xlarge + flavor: m1.mac + model: M1 commands: - command: curl https://artifactory.prd.it.unity3d.com/artifactory/stevedore-unity-internal/wrench-localapv/1-3-3_51b4e6affb4c10b90f92db9551b9dc80c5520794983600cff4fa925111db116d.zip -o wrench-localapv.zip - command: 7z x -aoa wrench-localapv.zip - command: pip install semver requests --index-url https://artifactory-slo.bf.unity3d.com/artifactory/api/pypi/pypi/simple - command: python PythonScripts/print_machine_info.py - - command: unity-downloader-cli -u trunk -c editor --path .Editor --fast + - command: unity-downloader-cli -u 6000.6/staging -c editor --path .Editor --fast --arch arm64 timeout: 20 retries: 3 - command: upm-pvp create-test-project testproject --packages "upm-ci~/packages/*.tgz" --filter "com.unity.netcode.gameobjects com.unity.netcode.gameobjects.tests" --unity .Editor @@ -909,10 +910,10 @@ validate_-_netcode_gameobjects_-_6000_6_-_macos13: UNITY_LICENSING_SERVER_DELETE_ULF: 0 UNITY_LICENSING_SERVER_TOOLSET: pro UPMPVP_ACK_UPMPVP_DOES_NO_API_VALIDATION: 1 - UPMPVP_CONTEXT_WRENCH: 2.9.0.0 + UPMPVP_CONTEXT_WRENCH: 3.2.0.0 metadata: Job Maintainers: '#rm-packageworks' - Wrench: 2.9.0.0 + Wrench: 3.2.0.0 labels: - Packages:netcode.gameobjects @@ -928,7 +929,7 @@ validate_-_netcode_gameobjects_-_6000_6_-_ubuntu2204: - command: 7z x -aoa wrench-localapv.zip - command: pip install semver requests --index-url https://artifactory-slo.bf.unity3d.com/artifactory/api/pypi/pypi/simple - command: python PythonScripts/print_machine_info.py - - command: unity-downloader-cli -u trunk -c editor --path .Editor --fast + - command: unity-downloader-cli -u 6000.6/staging -c editor --path .Editor --fast timeout: 20 retries: 3 - command: upm-pvp create-test-project testproject --packages "upm-ci~/packages/*.tgz" --filter "com.unity.netcode.gameobjects com.unity.netcode.gameobjects.tests" --unity .Editor @@ -979,10 +980,10 @@ validate_-_netcode_gameobjects_-_6000_6_-_ubuntu2204: UNITY_LICENSING_SERVER_DELETE_ULF: 0 UNITY_LICENSING_SERVER_TOOLSET: pro UPMPVP_ACK_UPMPVP_DOES_NO_API_VALIDATION: 1 - UPMPVP_CONTEXT_WRENCH: 2.9.0.0 + UPMPVP_CONTEXT_WRENCH: 3.2.0.0 metadata: Job Maintainers: '#rm-packageworks' - Wrench: 2.9.0.0 + Wrench: 3.2.0.0 labels: - Packages:netcode.gameobjects @@ -998,6 +999,217 @@ validate_-_netcode_gameobjects_-_6000_6_-_win10: - command: 7z x -aoa wrench-localapv.zip - command: pip install semver requests --index-url https://artifactory-slo.bf.unity3d.com/artifactory/api/pypi/pypi/simple - command: python PythonScripts/print_machine_info.py + - command: unity-downloader-cli -u 6000.6/staging -c editor --path .Editor --fast + timeout: 20 + retries: 3 + - command: upm-pvp create-test-project testproject --packages "upm-ci~/packages/*.tgz" --filter "com.unity.netcode.gameobjects com.unity.netcode.gameobjects.tests" --unity .Editor + timeout: 10 + retries: 1 + - command: echo No internal packages to add. + - command: upm-pvp test --unity .Editor --packages "upm-ci~/packages/*.tgz" --results upm-ci~/pvp + timeout: 40 + retries: 0 + - command: upm-pvp require "pkgprom-promote -PVP-29-2 rme" --results upm-ci~/pvp --exemptions upm-ci~/pvp/failures.json + timeout: 5 + retries: 0 + - command: upm-pvp require "rme PVP-160-1 supported" --results upm-ci~/pvp --exemptions upm-ci~/pvp/failures.json + timeout: 10 + retries: 0 + - command: 'UnifiedTestRunner.exe --testproject=testproject --editor-location=.Editor --clean-library --reruncount=1 --clean-library-on-rerun --artifacts-path=artifacts --suite=Editor --suite=Playmode "--ff={ops.upmpvpevidence.enable=true}" ' + timeout: 40 + retries: 1 + after: + - command: .yamato\generated-scripts\infrastructure-instability-detection-win.cmd + artifacts: + Crash Dumps: + paths: + - CrashDumps/** + packages: + paths: + - upm-ci~/packages/**/* + pvp-results: + paths: + - upm-ci~/pvp/**/* + browsable: onDemand + UTR: + paths: + - '*.log' + - '*.xml' + - artifacts/**/* + - testproject/Logs/** + - testproject/Library/*.log + - testproject/*.log + - testproject/Builds/*.log + - build/test-results/** + browsable: onDemand + dependencies: + - path: .yamato/wrench/package-pack-jobs.yml#package_pack_-_netcode_gameobjects + variables: + UNITY_LICENSING_SERVER_BASE_URL: http://unity-ci-licenses.hq.unity3d.com:8080/ + UNITY_LICENSING_SERVER_DELETE_NUL: 0 + UNITY_LICENSING_SERVER_DELETE_ULF: 0 + UNITY_LICENSING_SERVER_TOOLSET: pro + UPMPVP_ACK_UPMPVP_DOES_NO_API_VALIDATION: 1 + UPMPVP_CONTEXT_WRENCH: 3.2.0.0 + metadata: + Job Maintainers: '#rm-packageworks' + Wrench: 3.2.0.0 + labels: + - Packages:netcode.gameobjects + +# PVP Editor and Playmode tests for Validate - netcode.gameobjects - 6000.7 - macos13arm (6000.7 - MacOS). +validate_-_netcode_gameobjects_-_6000_7_-_macos13arm: + name: Validate - netcode.gameobjects - 6000.7 - macos13arm + agent: + image: package-ci/macos-13-arm64:v4 + type: Unity::VM::osx + flavor: m1.mac + model: M1 + commands: + - command: curl https://artifactory.prd.it.unity3d.com/artifactory/stevedore-unity-internal/wrench-localapv/1-3-3_51b4e6affb4c10b90f92db9551b9dc80c5520794983600cff4fa925111db116d.zip -o wrench-localapv.zip + - command: 7z x -aoa wrench-localapv.zip + - command: pip install semver requests --index-url https://artifactory-slo.bf.unity3d.com/artifactory/api/pypi/pypi/simple + - command: python PythonScripts/print_machine_info.py + - command: unity-downloader-cli -u trunk -c editor --path .Editor --fast --arch arm64 + timeout: 20 + retries: 3 + - command: upm-pvp create-test-project testproject --packages "upm-ci~/packages/*.tgz" --filter "com.unity.netcode.gameobjects com.unity.netcode.gameobjects.tests" --unity .Editor + timeout: 10 + retries: 1 + - command: echo No internal packages to add. + - command: upm-pvp test --unity .Editor --packages "upm-ci~/packages/*.tgz" --results upm-ci~/pvp + timeout: 40 + retries: 0 + - command: upm-pvp require "pkgprom-promote -PVP-29-2 rme" --results upm-ci~/pvp --exemptions upm-ci~/pvp/failures.json + timeout: 5 + retries: 0 + - command: upm-pvp require "rme PVP-160-1 supported" --results upm-ci~/pvp --exemptions upm-ci~/pvp/failures.json + timeout: 10 + retries: 0 + - command: 'UnifiedTestRunner --testproject=testproject --editor-location=.Editor --clean-library --reruncount=1 --clean-library-on-rerun --artifacts-path=artifacts --suite=Editor --suite=Playmode "--ff={ops.upmpvpevidence.enable=true}" ' + timeout: 40 + retries: 1 + after: + - command: bash .yamato/generated-scripts/infrastructure-instability-detection-mac.sh + artifacts: + Crash Dumps: + paths: + - CrashDumps/** + packages: + paths: + - upm-ci~/packages/**/* + pvp-results: + paths: + - upm-ci~/pvp/**/* + browsable: onDemand + UTR: + paths: + - '*.log' + - '*.xml' + - artifacts/**/* + - testproject/Logs/** + - testproject/Library/*.log + - testproject/*.log + - testproject/Builds/*.log + - build/test-results/** + browsable: onDemand + dependencies: + - path: .yamato/wrench/package-pack-jobs.yml#package_pack_-_netcode_gameobjects + variables: + UNITY_LICENSING_SERVER_BASE_URL: http://unity-ci-licenses.hq.unity3d.com:8080/ + UNITY_LICENSING_SERVER_DELETE_NUL: 0 + UNITY_LICENSING_SERVER_DELETE_ULF: 0 + UNITY_LICENSING_SERVER_TOOLSET: pro + UPMPVP_ACK_UPMPVP_DOES_NO_API_VALIDATION: 1 + UPMPVP_CONTEXT_WRENCH: 3.2.0.0 + metadata: + Job Maintainers: '#rm-packageworks' + Wrench: 3.2.0.0 + labels: + - Packages:netcode.gameobjects + +# PVP Editor and Playmode tests for Validate - netcode.gameobjects - 6000.7 - ubuntu2204 (6000.7 - Ubuntu). +validate_-_netcode_gameobjects_-_6000_7_-_ubuntu2204: + name: Validate - netcode.gameobjects - 6000.7 - ubuntu2204 + agent: + image: package-ci/ubuntu-22.04:v4 + type: Unity::VM + flavor: b1.large + commands: + - command: curl https://artifactory.prd.it.unity3d.com/artifactory/stevedore-unity-internal/wrench-localapv/1-3-3_51b4e6affb4c10b90f92db9551b9dc80c5520794983600cff4fa925111db116d.zip -o wrench-localapv.zip + - command: 7z x -aoa wrench-localapv.zip + - command: pip install semver requests --index-url https://artifactory-slo.bf.unity3d.com/artifactory/api/pypi/pypi/simple + - command: python PythonScripts/print_machine_info.py + - command: unity-downloader-cli -u trunk -c editor --path .Editor --fast + timeout: 20 + retries: 3 + - command: upm-pvp create-test-project testproject --packages "upm-ci~/packages/*.tgz" --filter "com.unity.netcode.gameobjects com.unity.netcode.gameobjects.tests" --unity .Editor + timeout: 10 + retries: 1 + - command: echo No internal packages to add. + - command: upm-pvp test --unity .Editor --packages "upm-ci~/packages/*.tgz" --results upm-ci~/pvp + timeout: 40 + retries: 0 + - command: upm-pvp require "pkgprom-promote -PVP-29-2 rme" --results upm-ci~/pvp --exemptions upm-ci~/pvp/failures.json + timeout: 5 + retries: 0 + - command: upm-pvp require "rme PVP-160-1 supported" --results upm-ci~/pvp --exemptions upm-ci~/pvp/failures.json + timeout: 10 + retries: 0 + - command: 'UnifiedTestRunner --testproject=testproject --editor-location=.Editor --clean-library --reruncount=1 --clean-library-on-rerun --artifacts-path=artifacts --suite=Editor --suite=Playmode "--ff={ops.upmpvpevidence.enable=true}" ' + timeout: 40 + retries: 1 + after: + - command: bash .yamato/generated-scripts/infrastructure-instability-detection-linux.sh + artifacts: + Crash Dumps: + paths: + - CrashDumps/** + packages: + paths: + - upm-ci~/packages/**/* + pvp-results: + paths: + - upm-ci~/pvp/**/* + browsable: onDemand + UTR: + paths: + - '*.log' + - '*.xml' + - artifacts/**/* + - testproject/Logs/** + - testproject/Library/*.log + - testproject/*.log + - testproject/Builds/*.log + - build/test-results/** + browsable: onDemand + dependencies: + - path: .yamato/wrench/package-pack-jobs.yml#package_pack_-_netcode_gameobjects + variables: + UNITY_LICENSING_SERVER_BASE_URL: http://unity-ci-licenses.hq.unity3d.com:8080/ + UNITY_LICENSING_SERVER_DELETE_NUL: 0 + UNITY_LICENSING_SERVER_DELETE_ULF: 0 + UNITY_LICENSING_SERVER_TOOLSET: pro + UPMPVP_ACK_UPMPVP_DOES_NO_API_VALIDATION: 1 + UPMPVP_CONTEXT_WRENCH: 3.2.0.0 + metadata: + Job Maintainers: '#rm-packageworks' + Wrench: 3.2.0.0 + labels: + - Packages:netcode.gameobjects + +# PVP Editor and Playmode tests for Validate - netcode.gameobjects - 6000.7 - win10 (6000.7 - Windows). +validate_-_netcode_gameobjects_-_6000_7_-_win10: + name: Validate - netcode.gameobjects - 6000.7 - win10 + agent: + image: package-ci/win10:v4 + type: Unity::VM + flavor: b1.large + commands: + - command: curl https://artifactory.prd.it.unity3d.com/artifactory/stevedore-unity-internal/wrench-localapv/1-3-3_51b4e6affb4c10b90f92db9551b9dc80c5520794983600cff4fa925111db116d.zip -o wrench-localapv.zip + - command: 7z x -aoa wrench-localapv.zip + - command: pip install semver requests --index-url https://artifactory-slo.bf.unity3d.com/artifactory/api/pypi/pypi/simple + - command: python PythonScripts/print_machine_info.py - command: unity-downloader-cli -u trunk -c editor --path .Editor --fast timeout: 20 retries: 3 @@ -1049,10 +1261,10 @@ validate_-_netcode_gameobjects_-_6000_6_-_win10: UNITY_LICENSING_SERVER_DELETE_ULF: 0 UNITY_LICENSING_SERVER_TOOLSET: pro UPMPVP_ACK_UPMPVP_DOES_NO_API_VALIDATION: 1 - UPMPVP_CONTEXT_WRENCH: 2.9.0.0 + UPMPVP_CONTEXT_WRENCH: 3.2.0.0 metadata: Job Maintainers: '#rm-packageworks' - Wrench: 2.9.0.0 + Wrench: 3.2.0.0 labels: - Packages:netcode.gameobjects diff --git a/.yamato/wrench/wrench_config.json b/.yamato/wrench/wrench_config.json index 9aa8ecfde1..585d0dfb22 100644 --- a/.yamato/wrench/wrench_config.json +++ b/.yamato/wrench/wrench_config.json @@ -39,7 +39,7 @@ }, "publishing_job": ".yamato/wrench/promotion-jobs.yml#publish_netcode_gameobjects", "branch_pattern": "ReleaseSlash", - "wrench_version": "2.9.0.0", + "wrench_version": "3.2.0.0", "pvp_exemption_path": ".yamato/wrench/pvp-exemptions.json", "cs_project_path": "Tools/CI/NGO.Cookbook.csproj" } \ No newline at end of file diff --git a/Tools/CI/NGO.Cookbook.csproj b/Tools/CI/NGO.Cookbook.csproj index 452a22131a..ef0ba3d3bc 100644 --- a/Tools/CI/NGO.Cookbook.csproj +++ b/Tools/CI/NGO.Cookbook.csproj @@ -8,11 +8,11 @@ - - - - - + + + + + diff --git a/com.unity.netcode.gameobjects/CHANGELOG.md b/com.unity.netcode.gameobjects/CHANGELOG.md index 1f7af6d540..3f6df83f11 100644 --- a/com.unity.netcode.gameobjects/CHANGELOG.md +++ b/com.unity.netcode.gameobjects/CHANGELOG.md @@ -21,7 +21,8 @@ Additional documentation and release notes are available at [Multiplayer Documen ### Fixed - +- Issue where NetworkRigidbodyBase was not applying rotation correctly when using Rigidbody2D. (#4012) +- Issue where NetworkRigidbodyBase was always checking the 3D rigid body's interpolation mode when determining if it is kinematic and needs to put the rigid body to sleep and then switch to interpolation. (#4012) ### Security @@ -34,6 +35,7 @@ Additional documentation and release notes are available at [Multiplayer Documen ### Added - Added support for Unity's Fast Enter Play Mode with domain reload disabled. (#3956) +- RPC messages now log any time they are not processed. (#3994) ### Changed diff --git a/com.unity.netcode.gameobjects/Editor/CodeGen/NetworkBehaviourILPP.cs b/com.unity.netcode.gameobjects/Editor/CodeGen/NetworkBehaviourILPP.cs index 04a72d7c9e..797197ec86 100644 --- a/com.unity.netcode.gameobjects/Editor/CodeGen/NetworkBehaviourILPP.cs +++ b/com.unity.netcode.gameobjects/Editor/CodeGen/NetworkBehaviourILPP.cs @@ -2152,14 +2152,14 @@ private bool GetReadMethodForParameter(TypeReference paramType, out MethodRefere } else #endif - if (paramType.Resolve().FullName == "Unity.Collections.NativeArray`1") - { - typeMethod = GetFastBufferReaderReadMethod(k_ReadValueTempMethodName, paramType); - } - else - { - typeMethod = GetFastBufferReaderReadMethod(k_ReadValueMethodName, paramType); - } + if (paramType.Resolve().FullName == "Unity.Collections.NativeArray`1") + { + typeMethod = GetFastBufferReaderReadMethod(k_ReadValueTempMethodName, paramType); + } + else + { + typeMethod = GetFastBufferReaderReadMethod(k_ReadValueMethodName, paramType); + } if (typeMethod != null) { methodRef = m_MainModule.ImportReference(typeMethod); diff --git a/com.unity.netcode.gameobjects/Editor/NetworkManagerHelper.cs b/com.unity.netcode.gameobjects/Editor/NetworkManagerHelper.cs index 5c7e554baf..a3224e6471 100644 --- a/com.unity.netcode.gameobjects/Editor/NetworkManagerHelper.cs +++ b/com.unity.netcode.gameobjects/Editor/NetworkManagerHelper.cs @@ -196,10 +196,10 @@ public bool NotifyUserOfNestedNetworkManager(NetworkManager networkManager, bool return isParented; } else // If we are no longer a child, then we can remove ourself from this list - if (transform.root == gameObject.transform) - { - s_LastKnownNetworkManagerParents.Remove(networkManager); - } + if (transform.root == gameObject.transform) + { + s_LastKnownNetworkManagerParents.Remove(networkManager); + } } if (!EditorApplication.isUpdating && isParented) { diff --git a/com.unity.netcode.gameobjects/Runtime/Components/Interpolator/BufferedLinearInterpolator.cs b/com.unity.netcode.gameobjects/Runtime/Components/Interpolator/BufferedLinearInterpolator.cs index 24eb580402..294cd9989f 100644 --- a/com.unity.netcode.gameobjects/Runtime/Components/Interpolator/BufferedLinearInterpolator.cs +++ b/com.unity.netcode.gameobjects/Runtime/Components/Interpolator/BufferedLinearInterpolator.cs @@ -468,19 +468,19 @@ internal T Update(float deltaTime, double tickLatencyAsTime, double minDeltaTime } } else // If the target is reached and we have no more state updates, we want to check to see if we need to reset. - if (m_BufferQueue.Count == 0) - { - // When the delta between the time sent and the current tick latency time-window is greater than the max delta time - // plus the minimum delta time (a rough estimate of time to wait before we consider rate of change equal to zero), - // we will want to reset the interpolator with the current known value. This prevents the next received state update's - // time to be calculated against the last calculated time which if there is an extended period of time between the two - // it would cause a large delta time period between the two states (i.e. it stops moving for a second or two and then - // starts moving again). - if ((tickLatencyAsTime - InterpolateState.Target.Value.TimeSent) > InterpolateState.MaxDeltaTime + minDeltaTime) + if (m_BufferQueue.Count == 0) { - InterpolateState.Reset(InterpolateState.CurrentValue); + // When the delta between the time sent and the current tick latency time-window is greater than the max delta time + // plus the minimum delta time (a rough estimate of time to wait before we consider rate of change equal to zero), + // we will want to reset the interpolator with the current known value. This prevents the next received state update's + // time to be calculated against the last calculated time which if there is an extended period of time between the two + // it would cause a large delta time period between the two states (i.e. it stops moving for a second or two and then + // starts moving again). + if ((tickLatencyAsTime - InterpolateState.Target.Value.TimeSent) > InterpolateState.MaxDeltaTime + minDeltaTime) + { + InterpolateState.Reset(InterpolateState.CurrentValue); + } } - } } m_NbItemsReceivedThisFrame = 0; return InterpolateState.CurrentValue; @@ -594,19 +594,19 @@ public T Update(float deltaTime, double renderTime, double serverTime) InterpolateState.TargetReached = IsApproximately(InterpolateState.CurrentValue, InterpolateState.Target.Value.Item, GetPrecision()); } else // If the target is reached and we have no more state updates, we want to check to see if we need to reset. - if (InterpolateState.TargetReached && m_BufferQueue.Count == 0) - { - // When the delta between the time sent and the current tick latency time-window is greater than the max delta time - // plus the minimum delta time (a rough estimate of time to wait before we consider rate of change equal to zero), - // we will want to reset the interpolator with the current known value. This prevents the next received state update's - // time to be calculated against the last calculated time which if there is an extended period of time between the two - // it would cause a large delta time period between the two states (i.e. it stops moving for a second or two and then - // starts moving again). - if ((renderTime - InterpolateState.Target.Value.TimeSent) > 0.3f) // If we haven't recevied anything within 300ms, assume we stopped motion. + if (InterpolateState.TargetReached && m_BufferQueue.Count == 0) { - InterpolateState.Reset(InterpolateState.CurrentValue); + // When the delta between the time sent and the current tick latency time-window is greater than the max delta time + // plus the minimum delta time (a rough estimate of time to wait before we consider rate of change equal to zero), + // we will want to reset the interpolator with the current known value. This prevents the next received state update's + // time to be calculated against the last calculated time which if there is an extended period of time between the two + // it would cause a large delta time period between the two states (i.e. it stops moving for a second or two and then + // starts moving again). + if ((renderTime - InterpolateState.Target.Value.TimeSent) > 0.3f) // If we haven't recevied anything within 300ms, assume we stopped motion. + { + InterpolateState.Reset(InterpolateState.CurrentValue); + } } - } m_NbItemsReceivedThisFrame = 0; return InterpolateState.CurrentValue; } diff --git a/com.unity.netcode.gameobjects/Runtime/Components/NetworkAnimator.cs b/com.unity.netcode.gameobjects/Runtime/Components/NetworkAnimator.cs index 246d6b8484..f87eee73ab 100644 --- a/com.unity.netcode.gameobjects/Runtime/Components/NetworkAnimator.cs +++ b/com.unity.netcode.gameobjects/Runtime/Components/NetworkAnimator.cs @@ -1181,25 +1181,25 @@ internal void CheckForAnimatorChanges() SendAnimStateRpc(m_AnimationMessage); } else - if (!IsServer && IsOwner) - { - SendServerAnimStateRpc(m_AnimationMessage); - } - else - { - // Just notify all remote clients and not the local server - m_TargetGroup.Clear(); - foreach (var clientId in LocalNetworkManager.ConnectionManager.ConnectedClientIds) + if (!IsServer && IsOwner) + { + SendServerAnimStateRpc(m_AnimationMessage); + } + else { - if (clientId == LocalNetworkManager.LocalClientId || !NetworkObject.Observers.Contains(clientId)) + // Just notify all remote clients and not the local server + m_TargetGroup.Clear(); + foreach (var clientId in LocalNetworkManager.ConnectionManager.ConnectedClientIds) { - continue; + if (clientId == LocalNetworkManager.LocalClientId || !NetworkObject.Observers.Contains(clientId)) + { + continue; + } + m_TargetGroup.Add(clientId); } - m_TargetGroup.Add(clientId); + m_RpcParams.Send.Target = m_TargetGroup.Target; + SendClientAnimStateRpc(m_AnimationMessage, m_RpcParams); } - m_RpcParams.Send.Target = m_TargetGroup.Target; - SendClientAnimStateRpc(m_AnimationMessage, m_RpcParams); - } } } @@ -1347,24 +1347,24 @@ private unsafe void WriteParameters(ref FastBufferWriter writer) } } else - if (cacheValue.Type == AnimationParamEnumWrapper.AnimatorControllerParameterBool) - { - var valueBool = m_Animator.GetBool(hash); - fixed (void* value = cacheValue.Value) + if (cacheValue.Type == AnimationParamEnumWrapper.AnimatorControllerParameterBool) { - UnsafeUtility.WriteArrayElement(value, 0, valueBool); - BytePacker.WriteValuePacked(writer, valueBool); + var valueBool = m_Animator.GetBool(hash); + fixed (void* value = cacheValue.Value) + { + UnsafeUtility.WriteArrayElement(value, 0, valueBool); + BytePacker.WriteValuePacked(writer, valueBool); + } } - } - else if (cacheValue.Type == AnimationParamEnumWrapper.AnimatorControllerParameterFloat) - { - var valueFloat = m_Animator.GetFloat(hash); - fixed (void* value = cacheValue.Value) + else if (cacheValue.Type == AnimationParamEnumWrapper.AnimatorControllerParameterFloat) { - UnsafeUtility.WriteArrayElement(value, 0, valueFloat); - BytePacker.WriteValuePacked(writer, valueFloat); + var valueFloat = m_Animator.GetFloat(hash); + fixed (void* value = cacheValue.Value) + { + UnsafeUtility.WriteArrayElement(value, 0, valueFloat); + BytePacker.WriteValuePacked(writer, valueFloat); + } } - } } } diff --git a/com.unity.netcode.gameobjects/Runtime/Components/NetworkTransform.cs b/com.unity.netcode.gameobjects/Runtime/Components/NetworkTransform.cs index 18fd9cd88f..63b43544bd 100644 --- a/com.unity.netcode.gameobjects/Runtime/Components/NetworkTransform.cs +++ b/com.unity.netcode.gameobjects/Runtime/Components/NetworkTransform.cs @@ -2516,23 +2516,23 @@ private bool CheckForStateChange(ref NetworkTransformState networkState, bool is } } else // Just apply the full local scale when synchronizing - if (SynchronizeScale) - { - var localScale = CachedTransform.localScale; - if (!UseHalfFloatPrecision) + if (SynchronizeScale) { + var localScale = CachedTransform.localScale; + if (!UseHalfFloatPrecision) + { - networkState.ScaleX = localScale.x; - networkState.ScaleY = localScale.y; - networkState.ScaleZ = localScale.z; - } - else - { - networkState.Scale = localScale; + networkState.ScaleX = localScale.x; + networkState.ScaleY = localScale.y; + networkState.ScaleZ = localScale.z; + } + else + { + networkState.Scale = localScale; + } + flagStates.MarkChanged(AxialType.Scale, true); + isScaleDirty = true; } - flagStates.MarkChanged(AxialType.Scale, true); - isScaleDirty = true; - } isDirty |= isPositionDirty || isRotationDirty || isScaleDirty; if (isDirty) @@ -3489,11 +3489,11 @@ private void NonAuthorityFinalizeSynchronization() } } else // Otherwise, just run through standard synchronization of this instance - if (!CanCommitToTransform) - { - ApplySynchronization(); - InternalInitialization(); - } + if (!CanCommitToTransform) + { + ApplySynchronization(); + InternalInitialization(); + } } } diff --git a/com.unity.netcode.gameobjects/Runtime/Connection/NetworkConnectionManager.cs b/com.unity.netcode.gameobjects/Runtime/Connection/NetworkConnectionManager.cs index 5a8724374c..cc44bd8783 100644 --- a/com.unity.netcode.gameobjects/Runtime/Connection/NetworkConnectionManager.cs +++ b/com.unity.netcode.gameobjects/Runtime/Connection/NetworkConnectionManager.cs @@ -968,18 +968,18 @@ internal void ProcessClientsToDisconnect() return (true, playerPrefabHash.Value); } else - if (NetworkManager.NetworkConfig.PlayerPrefab != null) - { - var networkObject = NetworkManager.NetworkConfig.PlayerPrefab.GetComponent(); - if (networkObject != null) + if (NetworkManager.NetworkConfig.PlayerPrefab != null) { - return (true, networkObject.GlobalObjectIdHash); - } - else - { - NetworkManager.Log.Error(new Logging.Context(LogLevel.Error, $"Player prefab {NetworkManager.NetworkConfig.PlayerPrefab.name} has no {nameof(NetworkObject)}!")); + var networkObject = NetworkManager.NetworkConfig.PlayerPrefab.GetComponent(); + if (networkObject != null) + { + return (true, networkObject.GlobalObjectIdHash); + } + else + { + NetworkManager.Log.Error(new Logging.Context(LogLevel.Error, $"Player prefab {NetworkManager.NetworkConfig.PlayerPrefab.name} has no {nameof(NetworkObject)}!")); + } } - } return (false, 0); } diff --git a/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs b/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs index 73cc6c7e2d..5008ee6075 100644 --- a/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs +++ b/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs @@ -2564,25 +2564,25 @@ internal bool ApplyNetworkParenting(bool removeParent = false, bool ignoreNotSpa return true; } else // If the parent still isn't spawned add this to the orphaned children and return false - if (!parentNetworkObject.IsSpawned) - { - OrphanChildren.Add(this); - return false; - } - else - { - // If we made it this far, go ahead and set the network parenting values - // with the WorldPoisitonSays value set to false - // Note: Since in-scene placed NetworkObjects are parented in the scene - // the default "assumption" is that children are parenting local space - // relative. - SetNetworkParenting(parentNetworkObject.NetworkObjectId, false); + if (!parentNetworkObject.IsSpawned) + { + OrphanChildren.Add(this); + return false; + } + else + { + // If we made it this far, go ahead and set the network parenting values + // with the WorldPoisitonSays value set to false + // Note: Since in-scene placed NetworkObjects are parented in the scene + // the default "assumption" is that children are parenting local space + // relative. + SetNetworkParenting(parentNetworkObject.NetworkObjectId, false); - // Set the cached parent - SetCachedParent(parentNetworkObject.transform); + // Set the cached parent + SetCachedParent(parentNetworkObject.transform); - return true; - } + return true; + } } // If we are removing the parent or our latest parent is not set, then remove the parent. @@ -3548,16 +3548,16 @@ internal void SceneChangedUpdate(Scene scene, bool notify = false) } } else // Otherwise, the client did not find the client to server scene handle - if (NetworkManagerOwner.LogLevel <= LogLevel.Developer) - { - // There could be a scenario where a user has some client-local scene loaded that they migrate the NetworkObject - // into, but that scenario seemed very edge case and under most instances a user should be notified that this - // server - client scene handle mismatch has occurred. It also seemed pertinent to make the message replicate to - // the server-side too. - NetworkLog.LogWarningServer($"[Client-{NetworkManagerOwner.LocalClientId}][{name}] Server - " + - $"client scene mismatch detected! Client-side scene handle ({SceneOriginHandle}) for scene ({gameObject.scene.name})" + - $"has no associated server side (network) scene handle!"); - } + if (NetworkManagerOwner.LogLevel <= LogLevel.Developer) + { + // There could be a scenario where a user has some client-local scene loaded that they migrate the NetworkObject + // into, but that scenario seemed very edge case and under most instances a user should be notified that this + // server - client scene handle mismatch has occurred. It also seemed pertinent to make the message replicate to + // the server-side too. + NetworkLog.LogWarningServer($"[Client-{NetworkManagerOwner.LocalClientId}][{name}] Server - " + + $"client scene mismatch detected! Client-side scene handle ({SceneOriginHandle}) for scene ({gameObject.scene.name})" + + $"has no associated server side (network) scene handle!"); + } OnMigratedToNewScene?.Invoke(); // Only the authority side will notify clients of non-parented NetworkObject scene changes diff --git a/com.unity.netcode.gameobjects/Runtime/Messaging/Messages/CreateObjectMessage.cs b/com.unity.netcode.gameobjects/Runtime/Messaging/Messages/CreateObjectMessage.cs index cf518a6216..b8f8af78fb 100644 --- a/com.unity.netcode.gameobjects/Runtime/Messaging/Messages/CreateObjectMessage.cs +++ b/com.unity.netcode.gameobjects/Runtime/Messaging/Messages/CreateObjectMessage.cs @@ -190,10 +190,10 @@ internal static void CreateObject(ref NetworkManager networkManager, ulong sende NetworkLog.LogErrorServer($"[{nameof(CreateObjectMessage)}][Duplicate-Broadcast] Detected duplicated object creation for {serializedObject.NetworkObjectId}!"); } else // Trap to make sure the owner is not receiving any messages it sent - if (networkManager.CMBServiceConnection && networkManager.LocalClientId == networkObject.OwnerClientId) - { - NetworkLog.LogWarning($"[{nameof(CreateObjectMessage)}][Client-{networkManager.LocalClientId}][Duplicate-CreateObjectMessage][Client Is Owner] Detected duplicated object creation for {networkObject.name}-{serializedObject.NetworkObjectId}!"); - } + if (networkManager.CMBServiceConnection && networkManager.LocalClientId == networkObject.OwnerClientId) + { + NetworkLog.LogWarning($"[{nameof(CreateObjectMessage)}][Client-{networkManager.LocalClientId}][Duplicate-CreateObjectMessage][Client Is Owner] Detected duplicated object creation for {networkObject.name}-{serializedObject.NetworkObjectId}!"); + } } else { diff --git a/com.unity.netcode.gameobjects/Runtime/SceneManagement/DefaultSceneManagerHandler.cs b/com.unity.netcode.gameobjects/Runtime/SceneManagement/DefaultSceneManagerHandler.cs index 76ea8feb04..dba918a679 100644 --- a/com.unity.netcode.gameobjects/Runtime/SceneManagement/DefaultSceneManagerHandler.cs +++ b/com.unity.netcode.gameobjects/Runtime/SceneManagement/DefaultSceneManagerHandler.cs @@ -360,13 +360,13 @@ public void SetClientSynchronizationMode(ref NetworkManager networkManager, Load return; } else // Warn users if they are changing this after there are clients already connected and synchronized - if (!networkManager.DistributedAuthorityMode && networkManager.ConnectedClientsIds.Count > (networkManager.IsHost ? 1 : 0) && sceneManager.ClientSynchronizationMode != mode) - { - if (NetworkLog.CurrentLogLevel <= LogLevel.Normal) + if (!networkManager.DistributedAuthorityMode && networkManager.ConnectedClientsIds.Count > (networkManager.IsHost ? 1 : 0) && sceneManager.ClientSynchronizationMode != mode) { - NetworkLog.LogWarning("Server is changing client synchronization mode after clients have been synchronized! It is recommended to do this before clients are connected!"); + if (NetworkLog.CurrentLogLevel <= LogLevel.Normal) + { + NetworkLog.LogWarning("Server is changing client synchronization mode after clients have been synchronized! It is recommended to do this before clients are connected!"); + } } - } // For additive client synchronization, we take into consideration scenes // already loaded. diff --git a/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkPrefabHandler.cs b/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkPrefabHandler.cs index c8aab939f4..ece7561161 100644 --- a/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkPrefabHandler.cs +++ b/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkPrefabHandler.cs @@ -342,10 +342,10 @@ internal void HandleNetworkPrefabDestroy(NetworkObject networkObjectInstance) } } else // Otherwise the NetworkObject is the source NetworkPrefab - if (m_PrefabAssetToPrefabHandler.TryGetValue(networkObjectInstanceHash, out var prefabInstanceHandler)) - { - prefabInstanceHandler.Destroy(networkObjectInstance); - } + if (m_PrefabAssetToPrefabHandler.TryGetValue(networkObjectInstanceHash, out var prefabInstanceHandler)) + { + prefabInstanceHandler.Destroy(networkObjectInstance); + } } /// diff --git a/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkSpawnManager.cs b/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkSpawnManager.cs index 46f52bc4f9..449ca04100 100644 --- a/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkSpawnManager.cs +++ b/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkSpawnManager.cs @@ -1820,10 +1820,10 @@ internal void OnDespawnObject([NotNull] NetworkObject networkObject, bool destro } } else - if (NetworkLog.CurrentLogLevel <= LogLevel.Developer) - { - NetworkLog.LogWarning($"{nameof(NetworkObject)} #{spawnedNetObj.NetworkObjectId} moved to the root because its parent {nameof(NetworkObject)} #{networkObject.NetworkObjectId} is destroyed"); - } + if (NetworkLog.CurrentLogLevel <= LogLevel.Developer) + { + NetworkLog.LogWarning($"{nameof(NetworkObject)} #{spawnedNetObj.NetworkObjectId} moved to the root because its parent {nameof(NetworkObject)} #{networkObject.NetworkObjectId} is destroyed"); + } } } diff --git a/com.unity.netcode.gameobjects/Tests/Runtime/DistributedAuthority/NetworkClientAndPlayerObjectTests.cs b/com.unity.netcode.gameobjects/Tests/Runtime/DistributedAuthority/NetworkClientAndPlayerObjectTests.cs index 5697833bbc..461d85a757 100644 --- a/com.unity.netcode.gameobjects/Tests/Runtime/DistributedAuthority/NetworkClientAndPlayerObjectTests.cs +++ b/com.unity.netcode.gameobjects/Tests/Runtime/DistributedAuthority/NetworkClientAndPlayerObjectTests.cs @@ -247,11 +247,11 @@ private bool ValidateAllPlayerObjects() success = false; } else - if (!client.SpawnManager.PlayerObjects.Contains(client.LocalClient.PlayerObject)) - { - m_ErrorLogLevel1.AppendLine($"[Client-{client.LocalClientId}] Local {nameof(NetworkSpawnManager.PlayerObjects)} does not contain {client.LocalClient.PlayerObject.name}!"); - success = false; - } + if (!client.SpawnManager.PlayerObjects.Contains(client.LocalClient.PlayerObject)) + { + m_ErrorLogLevel1.AppendLine($"[Client-{client.LocalClientId}] Local {nameof(NetworkSpawnManager.PlayerObjects)} does not contain {client.LocalClient.PlayerObject.name}!"); + success = false; + } } return success; diff --git a/com.unity.netcode.gameobjects/Tests/Runtime/NetworkShowHideTests.cs b/com.unity.netcode.gameobjects/Tests/Runtime/NetworkShowHideTests.cs index 6c09212a18..43f6eac7de 100644 --- a/com.unity.netcode.gameobjects/Tests/Runtime/NetworkShowHideTests.cs +++ b/com.unity.netcode.gameobjects/Tests/Runtime/NetworkShowHideTests.cs @@ -489,10 +489,10 @@ private bool Object1IsNotVisibileToClient() } } else - if (client.SpawnManager.SpawnedObjects[m_NetSpawnedObject1.NetworkObjectId].IsNetworkVisibleTo(m_ClientWithoutVisibility)) - { - m_ErrorLog.AppendLine($"Local instance of {m_NetSpawnedObject1.name} on Client-{client.LocalClientId} thinks Client-{m_ClientWithoutVisibility} still has visibility!"); - } + if (client.SpawnManager.SpawnedObjects[m_NetSpawnedObject1.NetworkObjectId].IsNetworkVisibleTo(m_ClientWithoutVisibility)) + { + m_ErrorLog.AppendLine($"Local instance of {m_NetSpawnedObject1.name} on Client-{client.LocalClientId} thinks Client-{m_ClientWithoutVisibility} still has visibility!"); + } } return m_ErrorLog.Length == 0; } diff --git a/com.unity.netcode.gameobjects/Tests/Runtime/NetworkTransform/NetworkTransformNonAuthorityTests.cs b/com.unity.netcode.gameobjects/Tests/Runtime/NetworkTransform/NetworkTransformNonAuthorityTests.cs index e5fb5a4ba9..5705d21942 100644 --- a/com.unity.netcode.gameobjects/Tests/Runtime/NetworkTransform/NetworkTransformNonAuthorityTests.cs +++ b/com.unity.netcode.gameobjects/Tests/Runtime/NetworkTransform/NetworkTransformNonAuthorityTests.cs @@ -419,12 +419,12 @@ private bool ShouldSyncAxis(bool first, bool second, bool lastValue) return false; } else - if (!first && !second) - { - // If both are disabled, then make the - // last one enabled. - return true; - } + if (!first && !second) + { + // If both are disabled, then make the + // last one enabled. + return true; + } } return Random.Range(start, 100) >= 50 ? false : true; } diff --git a/testproject/Assets/Tests/Manual/Scripts/NetworkPrefabPool.cs b/testproject/Assets/Tests/Manual/Scripts/NetworkPrefabPool.cs index 7821d2af27..5b045b1855 100644 --- a/testproject/Assets/Tests/Manual/Scripts/NetworkPrefabPool.cs +++ b/testproject/Assets/Tests/Manual/Scripts/NetworkPrefabPool.cs @@ -497,11 +497,11 @@ public void UpdateSpawnsPerSecond() StartSpawningBoxes(); } else //Handle case where spawning coroutine is running but we set our spawn rate to zero - if (SpawnsPerSecond == 0 && m_IsSpawningObjects) - { - m_IsSpawningObjects = false; - StopCoroutine(SpawnObjects()); - } + if (SpawnsPerSecond == 0 && m_IsSpawningObjects) + { + m_IsSpawningObjects = false; + StopCoroutine(SpawnObjects()); + } } } diff --git a/testproject/Assets/Tests/Manual/Scripts/NetworkPrefabPoolAdditive.cs b/testproject/Assets/Tests/Manual/Scripts/NetworkPrefabPoolAdditive.cs index f714d937de..0c4992cbd9 100644 --- a/testproject/Assets/Tests/Manual/Scripts/NetworkPrefabPoolAdditive.cs +++ b/testproject/Assets/Tests/Manual/Scripts/NetworkPrefabPoolAdditive.cs @@ -343,11 +343,11 @@ public void UpdateSpawnsPerSecond() StartSpawningBoxes(); } else //Handle case where spawning coroutine is running but we set our spawn rate to zero - if (SpawnsPerSecond == 0 && m_IsSpawningObjects) - { - m_IsSpawningObjects = false; - InternalStopCoroutine(); - } + if (SpawnsPerSecond == 0 && m_IsSpawningObjects) + { + m_IsSpawningObjects = false; + InternalStopCoroutine(); + } } diff --git a/testproject/Assets/Tests/Runtime/Animation/NetworkAnimatorTests.cs b/testproject/Assets/Tests/Runtime/Animation/NetworkAnimatorTests.cs index 474c9dabbe..dc2493ec28 100644 --- a/testproject/Assets/Tests/Runtime/Animation/NetworkAnimatorTests.cs +++ b/testproject/Assets/Tests/Runtime/Animation/NetworkAnimatorTests.cs @@ -528,10 +528,10 @@ private bool AllObserversSameLayerWeight(OwnerShipMode ownerShipMode, int layer, } } else - if (animatorTestHelper.Value.GetLayerWeight(layer) != targetWeight) - { - return false; - } + if (animatorTestHelper.Value.GetLayerWeight(layer) != targetWeight) + { + return false; + } } return true; } From f7ca6184928a958bcd5a7692ff6cd8015b904e96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Noellie=20V=C3=A9lez?= Date: Mon, 29 Jun 2026 20:32:30 +0200 Subject: [PATCH 18/26] chore: clean up SceneEventData internal fields and methods visibility (#4029) - Made list fields readonly - Reduced method visibility from internal to private where applicable - Replaced indexed for loops with foreach and inlined out variable declarations - Replaced ContainsKey + indexer with TryGetValue - Removed unused totalBytes variable --- .../Runtime/SceneManagement/SceneEventData.cs | 185 ++++++++---------- 1 file changed, 78 insertions(+), 107 deletions(-) diff --git a/com.unity.netcode.gameobjects/Runtime/SceneManagement/SceneEventData.cs b/com.unity.netcode.gameobjects/Runtime/SceneManagement/SceneEventData.cs index 0ad4e96864..4840944117 100644 --- a/com.unity.netcode.gameobjects/Runtime/SceneManagement/SceneEventData.cs +++ b/com.unity.netcode.gameobjects/Runtime/SceneManagement/SceneEventData.cs @@ -130,21 +130,21 @@ internal class SceneEventData : IDisposable /// was synchronizing (if so server will send another message back to the client informing the client of NetworkObjects to remove) /// spawned during an initial synchronization. /// - private List m_NetworkObjectsSync = new List(); + private readonly List m_NetworkObjectsSync = new List(); - private List m_DespawnedInSceneObjectsSync = new List(); + private readonly List m_DespawnedInSceneObjectsSync = new List(); /// /// Server Side Re-Synchronization: /// If there happens to be NetworkObjects in the final Event_Sync_Complete message that are no longer spawned, /// the server will compile a list and send back an Event_ReSync message to the client. /// - private List m_NetworkObjectsToBeRemoved = new List(); + private readonly List m_NetworkObjectsToBeRemoved = new List(); private bool m_HasInternalBuffer; - internal FastBufferReader InternalBuffer; + private FastBufferReader m_InternalBuffer; - private NetworkManager m_NetworkManager; + private readonly NetworkManager m_NetworkManager; internal List ClientsCompleted; internal List ClientsTimedOut; @@ -391,12 +391,12 @@ internal void AddDespawnedInSceneNetworkObjects() /// internal void AddNetworkObjectForSynch(uint sceneIndex, NetworkObject networkObject) { - if (!m_SceneNetworkObjects.ContainsKey(sceneIndex)) + if (!m_SceneNetworkObjects.TryGetValue(sceneIndex, out var sceneNetworkObject)) { - m_SceneNetworkObjects.Add(sceneIndex, new List()); + sceneNetworkObject = new List(); + m_SceneNetworkObjects.Add(sceneIndex, sceneNetworkObject); } - - m_SceneNetworkObjects[sceneIndex].Add(networkObject); + sceneNetworkObject.Add(networkObject); } /// @@ -571,7 +571,7 @@ internal void Serialize(FastBufferWriter writer) private unsafe void CopyInternalBuffer(ref FastBufferWriter writer) { - writer.WriteBytesSafe(InternalBuffer.GetUnsafePtrAtCurrentPosition(), InternalBuffer.Length); + writer.WriteBytesSafe(m_InternalBuffer.GetUnsafePtrAtCurrentPosition(), m_InternalBuffer.Length); } /// @@ -579,9 +579,9 @@ private unsafe void CopyInternalBuffer(ref FastBufferWriter writer) /// Called at the end of a event once the scene is loaded and scene placed NetworkObjects /// have been locally spawned /// - internal void WriteSceneSynchronizationData(FastBufferWriter writer) + private void WriteSceneSynchronizationData(FastBufferWriter writer) { - var builder = (StringBuilder)null; + StringBuilder builder = null; if (EnableSerializationLogs) { builder = new StringBuilder(); @@ -604,11 +604,10 @@ internal void WriteSceneSynchronizationData(FastBufferWriter writer) return; } - // Size Place Holder -- Start + // Size Placeholder -- Start // !!NOTE!!: Since this is a placeholder to be set after we know how much we have written, // for stream offset purposes this MUST not be a packed value! writer.WriteValueSafe(0); - int totalBytes = 0; // Write the number of NetworkObjects we are serializing writer.WriteValueSafe(m_NetworkObjectsSync.Count); @@ -619,16 +618,14 @@ internal void WriteSceneSynchronizationData(FastBufferWriter writer) var distributedAuthority = m_NetworkManager.DistributedAuthorityMode; // Serialize all NetworkObjects that are spawned - for (var i = 0; i < m_NetworkObjectsSync.Count; ++i) + foreach (var networkObject in m_NetworkObjectsSync) { - var networkObject = m_NetworkObjectsSync[i]; var noStart = writer.Position; // In distributed authority mode, we send the currently known observers of each NetworkObject to the client being synchronized. - var serializedObject = m_NetworkObjectsSync[i].Serialize(TargetClientId, distributedAuthority); + var serializedObject = networkObject.Serialize(TargetClientId, distributedAuthority); serializedObject.Serialize(writer); var noStop = writer.Position; - totalBytes += noStop - noStart; if (EnableSerializationLogs) { var offStart = noStart - (positionStart + sizeof(int)); @@ -645,16 +642,13 @@ internal void WriteSceneSynchronizationData(FastBufferWriter writer) // Write the number of despawned in-scene placed NetworkObjects writer.WriteValueSafe(m_DespawnedInSceneObjectsSync.Count); // Write the scene handle and GlobalObjectIdHash value - for (var i = 0; i < m_DespawnedInSceneObjectsSync.Count; ++i) + foreach (var despawnedInSceneObject in m_DespawnedInSceneObjectsSync) { - var noStart = writer.Position; - writer.WriteValueSafe(m_DespawnedInSceneObjectsSync[i].GetSceneOriginHandle()); - writer.WriteValueSafe(m_DespawnedInSceneObjectsSync[i].GlobalObjectIdHash); - var noStop = writer.Position; - totalBytes += noStop - noStart; + writer.WriteValueSafe(despawnedInSceneObject.GetSceneOriginHandle()); + writer.WriteValueSafe(despawnedInSceneObject.GlobalObjectIdHash); } - // Size Place Holder -- End + // Size Placeholder -- End var positionEnd = writer.Position; var bytesWritten = (uint)(positionEnd - (positionStart + sizeof(uint))); writer.Seek(positionStart); @@ -673,12 +667,12 @@ internal void WriteSceneSynchronizationData(FastBufferWriter writer) /// have been locally spawned /// Maximum number of objects that could theoretically be synchronized is 65536 /// - internal void SerializeScenePlacedObjects(FastBufferWriter writer) + private void SerializeScenePlacedObjects(FastBufferWriter writer) { - var numberOfObjects = (ushort)0; + ushort numberOfObjects = 0; var headPosition = writer.Position; - // Write our count place holder (must not be packed!) + // Write our count placeholder (must not be packed!) writer.WriteValueSafe((ushort)0); var distributedAuthority = m_NetworkManager.DistributedAuthorityMode; // If distributed authority mode and sending to the service, then ignore observers @@ -701,10 +695,10 @@ internal void SerializeScenePlacedObjects(FastBufferWriter writer) SortObjectsToSync(); // Serialize the sorted objects to sync. - foreach (var objectToSycn in m_NetworkObjectsSync) + foreach (var objectToSync in m_NetworkObjectsSync) { // Serialize the NetworkObject - var serializedObject = objectToSycn.Serialize(TargetClientId, distributedAuthority); + var serializedObject = objectToSync.Serialize(TargetClientId, distributedAuthority); serializedObject.Serialize(writer); numberOfObjects++; } @@ -712,10 +706,10 @@ internal void SerializeScenePlacedObjects(FastBufferWriter writer) // Write the number of despawned in-scene placed NetworkObjects writer.WriteValueSafe(m_DespawnedInSceneObjectsSync.Count); // Write the scene handle and GlobalObjectIdHash value - for (var i = 0; i < m_DespawnedInSceneObjectsSync.Count; ++i) + foreach (var despawnedInSceneObject in m_DespawnedInSceneObjectsSync) { - writer.WriteValueSafe(m_DespawnedInSceneObjectsSync[i].GetSceneOriginHandle()); - writer.WriteValueSafe(m_DespawnedInSceneObjectsSync[i].GlobalObjectIdHash); + writer.WriteValueSafe(despawnedInSceneObject.GetSceneOriginHandle()); + writer.WriteValueSafe(despawnedInSceneObject.GlobalObjectIdHash); } var tailPosition = writer.Position; @@ -802,7 +796,7 @@ internal void Deserialize(FastBufferReader reader) // be processed once we are done loading. m_HasInternalBuffer = true; // We use Allocator.Persistent since scene loading could take longer than 4 frames - InternalBuffer = new FastBufferReader(reader.GetUnsafePtrAtCurrentPosition(), Allocator.Persistent, reader.Length - reader.Position); + m_InternalBuffer = new FastBufferReader(reader.GetUnsafePtrAtCurrentPosition(), Allocator.Persistent, reader.Length - reader.Position); } break; } @@ -828,7 +822,7 @@ internal void Deserialize(FastBufferReader reader) /// into the internal buffer to be used throughout the synchronization process. /// /// - internal void CopySceneSynchronizationData(FastBufferReader reader) + private void CopySceneSynchronizationData(FastBufferReader reader) { m_NetworkObjectsSync.Clear(); reader.ReadValueSafe(out uint[] scenesToSynchronize); @@ -850,10 +844,10 @@ internal void CopySceneSynchronizationData(FastBufferReader reader) m_HasInternalBuffer = true; // We use Allocator.Persistent since scene synchronization will most likely take longer than 4 frames - InternalBuffer = new FastBufferReader(reader.GetUnsafePtrAtCurrentPosition(), Allocator.Persistent, sizeToCopy); + m_InternalBuffer = new FastBufferReader(reader.GetUnsafePtrAtCurrentPosition(), Allocator.Persistent, sizeToCopy); if (EnableSerializationLogs) { - LogArray(InternalBuffer.ToArray()); + LogArray(m_InternalBuffer.ToArray()); } } } @@ -868,12 +862,12 @@ internal void DeserializeScenePlacedObjects() try { // is not packed! - InternalBuffer.ReadValueSafe(out ushort newObjectsCount); + m_InternalBuffer.ReadValueSafe(out ushort newObjectsCount); var sceneObjects = new List(); for (ushort i = 0; i < newObjectsCount; i++) { var serializedObject = new NetworkObject.SerializedObject(); - serializedObject.Deserialize(InternalBuffer); + serializedObject.Deserialize(m_InternalBuffer); if (serializedObject.IsSceneObject) { @@ -881,7 +875,7 @@ internal void DeserializeScenePlacedObjects() m_NetworkManager.SceneManager.SetTheSceneBeingSynchronized(serializedObject.NetworkSceneHandle); } - var networkObject = NetworkObject.Deserialize(serializedObject, InternalBuffer, m_NetworkManager); + var networkObject = NetworkObject.Deserialize(serializedObject, m_InternalBuffer, m_NetworkManager); if (serializedObject.IsSceneObject) { @@ -900,7 +894,7 @@ internal void DeserializeScenePlacedObjects() } finally { - InternalBuffer.Dispose(); + m_InternalBuffer.Dispose(); m_HasInternalBuffer = false; } } @@ -912,7 +906,7 @@ internal void DeserializeScenePlacedObjects() /// client handles any returned values by the server. /// /// - internal void ReadClientReSynchronizationData(FastBufferReader reader) + private void ReadClientReSynchronizationData(FastBufferReader reader) { reader.ReadValueSafe(out uint[] networkObjectsToRemove); @@ -949,7 +943,7 @@ internal void ReadClientReSynchronizationData(FastBufferReader reader) /// the server will compile a list and send back an Event_ReSync message to the client. /// /// - internal void WriteClientReSynchronizationData(FastBufferWriter writer) + private void WriteClientReSynchronizationData(FastBufferWriter writer) { //Write how many objects need to be removed writer.WriteValueSafe(m_NetworkObjectsToBeRemoved.ToArray()); @@ -963,7 +957,7 @@ internal void WriteClientReSynchronizationData(FastBufferWriter writer) /// internal bool ClientNeedsReSynchronization() { - return (m_NetworkObjectsToBeRemoved.Count > 0); + return m_NetworkObjectsToBeRemoved.Count > 0; } /// @@ -973,7 +967,7 @@ internal bool ClientNeedsReSynchronization() /// have since been despawned. /// /// - internal void CheckClientSynchronizationResults(FastBufferReader reader) + private void CheckClientSynchronizationResults(FastBufferReader reader) { m_NetworkObjectsToBeRemoved.Clear(); reader.ReadValueSafe(out uint networkObjectIdCount); @@ -990,12 +984,12 @@ internal void CheckClientSynchronizationResults(FastBufferReader reader) /// /// Client Side: /// During the deserialization process of the servers Event_Sync, the client builds a list of - /// all NetworkObjectIds that were spawned. Upon responding to the server with the Event_Sync_Complete + /// all NetworkObjectIds that were spawned. Upon responding to the server with the Event_Sync_Complete, /// this list is included for the server to review over and determine if the client needs a minor resynchronization /// of NetworkObjects that might have been despawned while the client was processing the Event_Sync. /// /// - internal void WriteClientSynchronizationResults(FastBufferWriter writer) + private void WriteClientSynchronizationResults(FastBufferWriter writer) { //Write how many objects were spawned writer.WriteValueSafe((uint)m_NetworkObjectsSync.Count); @@ -1012,14 +1006,14 @@ internal void WriteClientSynchronizationResults(FastBufferWriter writer) private void DeserializeDespawnedInScenePlacedNetworkObjects() { // Process all de-spawned in-scene NetworkObjects for this network session - InternalBuffer.ReadValueSafe(out int despawnedObjectsCount); + m_InternalBuffer.ReadValueSafe(out int despawnedObjectsCount); var sceneCache = new Dictionary>(); for (int i = 0; i < despawnedObjectsCount; i++) { // We just need to get the scene - InternalBuffer.ReadValueSafe(out NetworkSceneHandle networkSceneHandle); - InternalBuffer.ReadValueSafe(out uint globalObjectIdHash); + m_InternalBuffer.ReadValueSafe(out NetworkSceneHandle networkSceneHandle); + m_InternalBuffer.ReadValueSafe(out uint globalObjectIdHash); // Check if we already have processed the objects in this scene if (!sceneCache.TryGetValue(networkSceneHandle, out var sceneRelativeNetworkObjects)) @@ -1060,15 +1054,10 @@ private void DeserializeDespawnedInScenePlacedNetworkObjects() // Since this is a NetworkObject that was never spawned, we just need to send a notification // out that it was despawned so users can make adjustments despawnedObject.InvokeBehaviourNetworkDespawn(); - if (!m_NetworkManager.SceneManager.ScenePlacedObjects.ContainsKey(globalObjectIdHash)) - { - m_NetworkManager.SceneManager.ScenePlacedObjects.Add(globalObjectIdHash, new Dictionary()); - } - if (!m_NetworkManager.SceneManager.ScenePlacedObjects[globalObjectIdHash].ContainsKey(despawnedObject.GetSceneOriginHandle())) - { - m_NetworkManager.SceneManager.ScenePlacedObjects[globalObjectIdHash].Add(despawnedObject.GetSceneOriginHandle(), despawnedObject); - } + m_NetworkManager.SceneManager.ScenePlacedObjects.TryAdd(globalObjectIdHash, new Dictionary()); + + m_NetworkManager.SceneManager.ScenePlacedObjects[globalObjectIdHash].TryAdd(despawnedObject.GetSceneOriginHandle(), despawnedObject); } else { @@ -1086,7 +1075,7 @@ private void DeserializeDespawnedInScenePlacedNetworkObjects() /// internal void SynchronizeSceneNetworkObjects(NetworkManager networkManager) { - var builder = (StringBuilder)null; + StringBuilder builder = null; if (EnableSerializationLogs) { builder = new StringBuilder(); @@ -1095,30 +1084,30 @@ internal void SynchronizeSceneNetworkObjects(NetworkManager networkManager) try { // Process all spawned NetworkObjects for this network session - InternalBuffer.ReadValueSafe(out int newObjectsCount); + m_InternalBuffer.ReadValueSafe(out int newObjectsCount); if (EnableSerializationLogs) { - builder.AppendLine($"[Read][Synchronize Objects][WPos: {InternalBuffer.Position}][NO-Count: {newObjectsCount}] Begin:"); + builder.AppendLine($"[Read][Synchronize Objects][WPos: {m_InternalBuffer.Position}][NO-Count: {newObjectsCount}] Begin:"); } for (int i = 0; i < newObjectsCount; i++) { - var noStart = InternalBuffer.Position; + var noStart = m_InternalBuffer.Position; var serializedObject = new NetworkObject.SerializedObject(); - serializedObject.Deserialize(InternalBuffer); + serializedObject.Deserialize(m_InternalBuffer); // If the sceneObject is in-scene placed, then set the scene being synchronized if (serializedObject.IsSceneObject) { m_NetworkManager.SceneManager.SetTheSceneBeingSynchronized(serializedObject.NetworkSceneHandle); } - var spawnedNetworkObject = NetworkObject.Deserialize(serializedObject, InternalBuffer, networkManager); + var spawnedNetworkObject = NetworkObject.Deserialize(serializedObject, m_InternalBuffer, networkManager); - var noStop = InternalBuffer.Position; + var noStop = m_InternalBuffer.Position; if (EnableSerializationLogs) { builder.AppendLine($"[Head: {noStart}][Tail: {noStop}][Size: {noStop - noStart}][{spawnedNetworkObject.name}][NID-{spawnedNetworkObject.NetworkObjectId}][Children: {spawnedNetworkObject.ChildNetworkBehaviours.Count}]"); - LogArray(InternalBuffer.ToArray(), noStart, noStop, builder); + LogArray(m_InternalBuffer.ToArray(), noStart, noStop, builder); } // If we failed to deserialize the NetworkObject then don't add null to the list if (spawnedNetworkObject != null) @@ -1157,7 +1146,7 @@ internal void SynchronizeSceneNetworkObjects(NetworkManager networkManager) } finally { - InternalBuffer.Dispose(); + m_InternalBuffer.Dispose(); m_HasInternalBuffer = false; } } @@ -1166,7 +1155,7 @@ internal void SynchronizeSceneNetworkObjects(NetworkManager networkManager) /// Writes the all clients loaded or unloaded completed and timed out lists /// /// - internal void WriteSceneEventProgressDone(FastBufferWriter writer) + private void WriteSceneEventProgressDone(FastBufferWriter writer) { writer.WriteValueSafe((ushort)ClientsCompleted.Count); foreach (var clientId in ClientsCompleted) @@ -1185,7 +1174,7 @@ internal void WriteSceneEventProgressDone(FastBufferWriter writer) /// Reads the all clients loaded or unloaded completed and timed out lists /// /// - internal void ReadSceneEventProgressDone(FastBufferReader reader) + private void ReadSceneEventProgressDone(FastBufferReader reader) { reader.ReadValueSafe(out ushort completedCount); ClientsCompleted = new List(); @@ -1227,7 +1216,7 @@ private void SerializeObjectsMovedIntoNewScene(FastBufferWriter writer) writer.WriteValueSafe(sceneManager.ObjectsMigratedIntoNewScene.Count); foreach (var sceneHandleObjects in sceneManager.ObjectsMigratedIntoNewScene) { - if (!sceneManager.ObjectsMigratedIntoNewScene[sceneHandleObjects.Key].ContainsKey(ownerId)) + if (!sceneHandleObjects.Value.ContainsKey(ownerId)) { throw new Exception($"Trying to send object scene migration for Client-{ownerId} but the client has no entries to send!"); } @@ -1251,40 +1240,31 @@ private void DeserializeObjectsMovedIntoNewScene(FastBufferReader reader) var sceneManager = m_NetworkManager.SceneManager; var spawnManager = m_NetworkManager.SpawnManager; - var numberOfScenes = 0; - NetworkSceneHandle sceneHandle; - var objectCount = 0; - var networkObjectId = (ulong)0; - - var ownerID = (ulong)0; - reader.ReadValueSafe(out ownerID); + reader.ReadValueSafe(out ulong ownerID); m_OwnerId = ownerID; - reader.ReadValueSafe(out numberOfScenes); + reader.ReadValueSafe(out int numberOfScenes); for (int i = 0; i < numberOfScenes; i++) { - reader.ReadValueSafe(out sceneHandle); + reader.ReadValueSafe(out NetworkSceneHandle sceneHandle); if (!sceneManager.ObjectsMigratedIntoNewScene.TryGetValue(sceneHandle, out var migratedObjects)) { migratedObjects = new Dictionary>(); sceneManager.ObjectsMigratedIntoNewScene.Add(sceneHandle, migratedObjects); } - if (!migratedObjects.ContainsKey(ownerID)) - { - migratedObjects.Add(ownerID, new List()); - } + migratedObjects.TryAdd(ownerID, new List()); - reader.ReadValueSafe(out objectCount); + reader.ReadValueSafe(out int objectCount); for (int j = 0; j < objectCount; j++) { - reader.ReadValueSafe(out networkObjectId); - if (!spawnManager.SpawnedObjects.ContainsKey(networkObjectId)) + reader.ReadValueSafe(out ulong networkObjectId); + if (!spawnManager.SpawnedObjects.TryGetValue(networkObjectId, out var networkObject)) { NetworkLog.LogError($"[Object Scene Migration] Trying to synchronize NetworkObjectId ({networkObjectId}) but it was not spawned or no longer exists!!"); continue; } - var networkObject = spawnManager.SpawnedObjects[networkObjectId]; + // Add NetworkObject scene migration to ObjectsMigratedIntoNewScene dictionary that is processed migratedObjects[ownerID].Add(networkObject); } @@ -1301,15 +1281,8 @@ private void DeserializeObjectsMovedIntoNewScene(FastBufferReader reader) private void DeferObjectsMovedIntoNewScene(FastBufferReader reader) { var sceneManager = m_NetworkManager.SceneManager; - var spawnManager = m_NetworkManager.SpawnManager; - var ownerId = (ulong)0; - var numberOfScenes = 0; - NetworkSceneHandle sceneHandle; - var objectCount = 0; - var networkObjectId = (ulong)0; - - reader.ReadValueSafe(out ownerId); + reader.ReadValueSafe(out ulong ownerId); var deferredObjectsMovedEvent = new NetworkSceneManager.DeferredObjectsMovedEvent() { @@ -1317,17 +1290,16 @@ private void DeferObjectsMovedIntoNewScene(FastBufferReader reader) ObjectsMigratedTable = new Dictionary>(), }; - - reader.ReadValueSafe(out numberOfScenes); + reader.ReadValueSafe(out int numberOfScenes); for (int i = 0; i < numberOfScenes; i++) { - reader.ReadValueSafe(out sceneHandle); + reader.ReadValueSafe(out NetworkSceneHandle sceneHandle); var objectsMigrated = new List(); deferredObjectsMovedEvent.ObjectsMigratedTable.Add(sceneHandle, objectsMigrated); - reader.ReadValueSafe(out objectCount); + reader.ReadValueSafe(out int objectCount); for (int j = 0; j < objectCount; j++) { - reader.ReadValueSafe(out networkObjectId); + reader.ReadValueSafe(out ulong networkObjectId); objectsMigrated.Add(networkObjectId); } } @@ -1351,10 +1323,9 @@ internal void ProcessDeferredObjectSceneChangedEvents() migratedObjects = new Dictionary>(); sceneManager.ObjectsMigratedIntoNewScene.Add(keyEntry.Key, migratedObjects); } - if (!migratedObjects.ContainsKey(objectsMovedEvent.OwnerId)) - { - migratedObjects.Add(objectsMovedEvent.OwnerId, new List()); - } + + migratedObjects.TryAdd(objectsMovedEvent.OwnerId, new List()); + var objectList = migratedObjects[objectsMovedEvent.OwnerId]; foreach (var objectId in keyEntry.Value) { @@ -1364,9 +1335,9 @@ internal void ProcessDeferredObjectSceneChangedEvents() continue; } - if (!migratedObjects[objectsMovedEvent.OwnerId].Contains(networkObject)) + if (!objectList.Contains(networkObject)) { - migratedObjects[objectsMovedEvent.OwnerId].Add(networkObject); + objectList.Add(networkObject); } } } @@ -1382,7 +1353,7 @@ public void Dispose() { if (m_HasInternalBuffer) { - InternalBuffer.Dispose(); + m_InternalBuffer.Dispose(); m_HasInternalBuffer = false; } } From e6ecb65732033b377e13d9db868a1933a1fd0cec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Noellie=20V=C3=A9lez?= Date: Tue, 30 Jun 2026 23:36:42 +0200 Subject: [PATCH 19/26] chore: cache HasAuthority to avoid recomputing it on every access (#4026) refactor: precalculate HasAuthority into cached m_HasAuthority field - Replaced InternalHasAuthority() with private m_HasAuthority field - Set m_HasAuthority on Spawn (before SpawnInternal), cleared on despawn, updated on ownership change - Replaced all internal HasAuthority call sites with direct m_HasAuthority field access - Updated public HasAuthority property to return cached value when spawned - Moved network object setup from NetworkSpawnManager.SpawnNetworkObjectLocallyCommon to NetworkObject.SetupOnSpawn * fix vettng test --------- Co-authored-by: Emma --- .../Runtime/Core/NetworkObject.cs | 195 ++++++++++++------ .../Runtime/Logging/NetworkLog.cs | 8 + .../Runtime/Spawning/NetworkSpawnManager.cs | 118 ++--------- .../DistributedAuthorityCodecTests.cs | 61 ------ .../NetworkObjectOnSpawnTests.cs | 28 +-- .../ProjectSettings/ProjectSettings.asset | 2 +- 6 files changed, 165 insertions(+), 247 deletions(-) diff --git a/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs b/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs index 5008ee6075..3423c05a07 100644 --- a/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs +++ b/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs @@ -416,7 +416,7 @@ public void DeferDespawn(int tickOffset, bool destroy = true) return; } - if (!HasAuthority) + if (!m_HasAuthority) { if (NetworkManagerOwner.LogLevel <= LogLevel.Error) { @@ -633,7 +633,7 @@ public bool SetOwnershipLock(bool lockOwnership = true) } // If we don't have authority exit early - if (!HasAuthority) + if (!m_HasAuthority) { if (NetworkManager.LogLevel <= LogLevel.Error) { @@ -909,7 +909,7 @@ internal void OwnershipRequest(ulong clientRequestingOwnership) // This action is always authorized as long as the client still has authority. // We need to pass in that this is a request approval ownership change. - NetworkManagerOwner.SpawnManager.ChangeOwnership(this, clientRequestingOwnership, HasAuthority, true); + NetworkManagerOwner.SpawnManager.ChangeOwnership(this, clientRequestingOwnership, m_HasAuthority, true); } else { @@ -1155,14 +1155,9 @@ public bool HasOwnershipStatus(OwnershipStatus status) /// /// When in client-server mode, authority should is not considered the same as ownership. /// - public bool HasAuthority => InternalHasAuthority(); + public bool HasAuthority => IsSpawned ? m_HasAuthority : !NetworkManager.DistributedAuthorityMode && NetworkManager.IsServer; - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private bool InternalHasAuthority() - { - var networkManager = NetworkManager; - return networkManager.DistributedAuthorityMode ? OwnerClientId == networkManager.LocalClientId : networkManager.IsServer; - } + private bool m_HasAuthority; /// /// The NetworkManager that owns this NetworkObject. @@ -1448,10 +1443,7 @@ public bool IsNetworkVisibleTo(ulong clientId) /// internal Scene SceneOrigin { - get - { - return m_SceneOrigin; - } + get => m_SceneOrigin; set { @@ -1471,13 +1463,8 @@ internal Scene SceneOrigin /// internal NetworkSceneHandle GetSceneOriginHandle() { - if (SceneOriginHandle.IsEmpty() && IsSpawned && InScenePlaced) - { - if (NetworkManager.LogLevel <= LogLevel.Error) - { - NetworkLog.LogErrorServer($"{nameof(GetSceneOriginHandle)} called when {nameof(SceneOriginHandle)} is still zero but the {nameof(NetworkObject)} is already spawned!"); - } - } + NetworkLog.InternalAssert(!(IsSpawned && InScenePlaced && SceneOriginHandle.IsEmpty()), $"Spawned in scene placed NetworkObject {name} should always have a valid SceneOriginHandle"); + return !SceneOriginHandle.IsEmpty() ? SceneOriginHandle : gameObject.scene.handle; } @@ -1506,7 +1493,7 @@ public void NetworkShow(ulong clientId) return; } - if (!HasAuthority) + if (!m_HasAuthority) { if (NetworkManagerOwner.DistributedAuthorityMode) { @@ -1601,7 +1588,7 @@ public void NetworkHide(ulong clientId) return; } - if (!HasAuthority) + if (!m_HasAuthority) { if (NetworkManagerOwner.DistributedAuthorityMode) { @@ -1760,7 +1747,7 @@ private void OnDestroy() { // An authorized destroy is when done by the authority instance or done due to a scene event and the NetworkObject // was marked as destroy pending scene event (which means the destroy with scene property was set). - var isAuthorityDestroy = HasAuthority || NetworkManager.DAHost || DestroyPendingSceneEvent; + var isAuthorityDestroy = m_HasAuthority || NetworkManager.DAHost || DestroyPendingSceneEvent; // If the NetworkObject's GameObject is still valid and the scene is still valid and loaded, then we are still valid var isStillValid = gameObject != null && gameObject.scene.IsValid() && gameObject.scene.isLoaded; @@ -2005,8 +1992,7 @@ public NetworkObject InstantiateAndSpawn(NetworkManager networkManager, ulong ow /// Should the object be destroyed when the scene is changed public void Spawn(bool destroyWithScene = false) { - var networkManager = NetworkManager; - var clientId = networkManager.DistributedAuthorityMode ? networkManager.LocalClientId : NetworkManager.ServerClientId; + var clientId = NetworkManager.DistributedAuthorityMode ? NetworkManager.LocalClientId : NetworkManager.ServerClientId; SpawnInternal(destroyWithScene, clientId, false); } @@ -2058,10 +2044,60 @@ public void Despawn(bool destroy = true) NetworkManagerOwner.SpawnManager.DespawnObject(this, destroy); } + internal void SetupOnSpawn(ulong networkId, bool isPlayerObject, ulong ownerClientId, bool destroyWithScene) + { + NetworkObjectId = networkId; + IsPlayerObject = isPlayerObject; + OwnerClientId = ownerClientId; + // When spawned, previous owner is always the first assigned owner + PreviousOwnerId = ownerClientId; + m_HasAuthority = NetworkManagerOwner.DistributedAuthorityMode ? OwnerClientId == NetworkManagerOwner.LocalClientId : NetworkManagerOwner.IsServer; + IsSpawned = true; + + // If this is the player, and the client is the owner, then lock ownership by default + if (NetworkManagerOwner.DistributedAuthorityMode && NetworkManagerOwner.LocalClientId == ownerClientId && isPlayerObject) + { + AddOwnershipExtended(OwnershipStatusExtended.Locked); + } + + if (IsSpawnAuthority) + { + SetupObservers(); + } + + /* + * Setup scene related settings + */ + DestroyWithScene = InScenePlaced || destroyWithScene; + if (InScenePlaced) + { + // Always check to make sure our scene of origin is properly set for in-scene placed NetworkObjects + // Note: Always check SceneOriginHandle directly at this specific location. + if (SceneOriginHandle.IsEmpty()) + { + SceneOrigin = gameObject.scene; + } + + // If we are an in-scene placed NetworkObject and our InScenePlacedSourceGlobalObjectIdHash is set + // then assign this to the PrefabGlobalObjectIdHash + if (InScenePlacedSourceGlobalObjectIdHash != 0) + { + PrefabGlobalObjectIdHash = InScenePlacedSourceGlobalObjectIdHash; + } + } + else if (ActiveSceneSynchronization) + { + // Just in case it is a recycled NetworkObject, unsubscribe first + SceneManager.activeSceneChanged -= CurrentlyActiveSceneChanged; + SceneManager.activeSceneChanged += CurrentlyActiveSceneChanged; + } + } + internal void ResetOnDespawn() { // Always clear out the observers list when despawned Observers.Clear(); + m_HasAuthority = false; IsSpawnAuthority = false; IsSpawned = false; DeferredDespawnTick = 0; @@ -2069,6 +2105,62 @@ internal void ResetOnDespawn() RemoveOwnershipExtended(OwnershipStatusExtended.Locked | OwnershipStatusExtended.Requested); } + internal void SetupObservers() + { + NetworkLog.InternalAssert(IsSpawnAuthority, "This function should only be called on the authority."); + + if (!SpawnWithObservers) + { + if (NetworkManagerOwner.DistributedAuthorityMode) + { + // Always add the owner/authority in DA mode even if SpawnWithObservers is false + // (authority should not take into consideration networkObject.CheckObjectVisibility when SpawnWithObservers is false) + AddObserver(OwnerClientId); + } + + return; + } + + // If running as a server only, then make sure to always add the server's client identifier + if (NetworkManagerOwner.IsServer && !NetworkManagerOwner.IsHost) + { + AddObserver(NetworkManager.ServerClientId); + } + + // If SpawnWithObservers is set, + // then add all connected clients as observers + foreach (var clientId in NetworkManagerOwner.ConnectedClientsIds) + { + // If CheckObjectVisibility has a callback, then allow that method determine who the observers are. + if (CheckObjectVisibility != null && !CheckObjectVisibility(clientId)) + { + continue; + } + AddObserver(clientId); + } + + // Intentionally checking as opposed to just assigning in order to generate notification. + if (!Observers.Contains(OwnerClientId)) + { + // The owner only needs to always be included in DA mode. + if (NetworkManagerOwner.DistributedAuthorityMode) + { + if (NetworkManager.LogLevel <= LogLevel.Error) + { + NetworkLog.LogError($"Client-{OwnerClientId} is the owner of {name} but is not an observer! Adding owner as an observer!"); + } + AddObserver(OwnerClientId); + } + else + { + if (NetworkManager.LogLevel <= LogLevel.Developer) + { + NetworkLog.LogWarning($"Client-{OwnerClientId} is the owner of {name} but is not an observer! This may cause issues"); + } + } + } + } + /// /// Removes all ownership of an object from any client. Can only be called from server /// @@ -2099,7 +2191,7 @@ public void ChangeOwnership(ulong newOwnerClientId) } return; } - NetworkManagerOwner.SpawnManager.ChangeOwnership(this, newOwnerClientId, HasAuthority); + NetworkManagerOwner.SpawnManager.ChangeOwnership(this, newOwnerClientId, m_HasAuthority); } /// @@ -2108,20 +2200,18 @@ public void ChangeOwnership(ulong newOwnerClientId) /// internal void InvokeBehaviourOnOwnershipChanged(ulong originalOwnerClientId, ulong newOwnerClientId) { - if (!IsSpawned) - { - if (NetworkManager.LogLevel <= LogLevel.Error) - { - NetworkLog.LogErrorServer($"[{name}][Attempted behavior invoke on ownership changed before {nameof(NetworkObject)} was spawned]"); - } - return; - } + NetworkLog.InternalAssert(IsSpawned, "[{name}][Attempted behavior invoke on ownership changed before {nameof(NetworkObject)} was spawned]"); var distributedAuthorityMode = NetworkManagerOwner.DistributedAuthorityMode; var isServer = NetworkManagerOwner.IsServer; var isPreviousOwner = originalOwnerClientId == NetworkManagerOwner.LocalClientId; var isNewOwner = newOwnerClientId == NetworkManagerOwner.LocalClientId; + if (distributedAuthorityMode) + { + m_HasAuthority = isNewOwner; + } + if (distributedAuthorityMode || isPreviousOwner) { NetworkManagerOwner.SpawnManager.UpdateOwnershipTable(this, originalOwnerClientId, true); @@ -2334,7 +2424,7 @@ public bool TrySetParent(NetworkObject parent, bool worldPositionStays = true) // DANGO-TODO: Do we want to worry about ownership permissions here? // It wouldn't make sense to not allow parenting, but keeping this note here as a reminder. - var isAuthority = HasAuthority || (AllowOwnerToParent && IsOwner); + var isAuthority = m_HasAuthority || (AllowOwnerToParent && IsOwner); // If we don't have authority and we are not shutting down, then don't allow any parenting. // If we are shutting down and don't have authority then allow it. @@ -2409,7 +2499,7 @@ private void OnTransformParentChanged() // With distributed authority, we need to track "valid authoritative" parenting changes. // So, either the authority or AuthorityAppliedParenting is considered a "valid parenting change". - var isParentingAuthority = HasAuthority || AuthorityAppliedParenting || (AllowOwnerToParent && IsOwner); + var isParentingAuthority = m_HasAuthority || AuthorityAppliedParenting || (AllowOwnerToParent && IsOwner); // If we are spawned and don't have authority; reset the parent back to the cached parent and exit if (!isParentingAuthority) { @@ -2663,8 +2753,6 @@ internal void InvokeBehaviourNetworkPreSpawn() internal void InvokeBehaviourNetworkSpawn() { - NetworkManagerOwner.SpawnManager.UpdateOwnershipTable(this, OwnerClientId); - // Always invoke all InternalOnNetworkSpawn methods on each child NetworkBehaviour // ** before ** invoking OnNetworkSpawn. // This assures all NetworkVariables and RPC related tables have been initialized @@ -3465,29 +3553,13 @@ internal static NetworkObject Deserialize(in SerializedObject serializedObject, return networkObject; } - /// - /// Subscribes to changes in the currently active scene - /// - /// - /// Only for dynamically spawned NetworkObjects - /// - internal void SubscribeToActiveSceneForSynch() - { - if (ActiveSceneSynchronization) - { - if (!InScenePlaced) - { - // Just in case it is a recycled NetworkObject, unsubscribe first - SceneManager.activeSceneChanged -= CurrentlyActiveSceneChanged; - SceneManager.activeSceneChanged += CurrentlyActiveSceneChanged; - } - } - } - /// /// If AutoSynchActiveScene is enabled, then this is the callback that handles updating /// a NetworkObject's scene information. /// + /// + /// Should only be used for dynamically spawned NetworkObjects + /// private void CurrentlyActiveSceneChanged(Scene current, Scene next) { // Early exit if the NetworkObject is not spawned, is an in-scene placed NetworkObject, @@ -3526,15 +3598,14 @@ internal void SceneChangedUpdate(Scene scene, bool notify = false) return; } - var isAuthority = HasAuthority; SceneOriginHandle = scene.handle; // non-authority needs to update the NetworkSceneHandle - if (!isAuthority && NetworkManagerOwner.SceneManager.ClientSceneHandleToServerSceneHandle.ContainsKey(SceneOriginHandle)) + if (!m_HasAuthority && NetworkManagerOwner.SceneManager.ClientSceneHandleToServerSceneHandle.ContainsKey(SceneOriginHandle)) { NetworkSceneHandle = NetworkManagerOwner.SceneManager.ClientSceneHandleToServerSceneHandle[SceneOriginHandle]; } - else if (isAuthority) + else if (m_HasAuthority) { // Since the authority is the source of truth for the NetworkSceneHandle, // the NetworkSceneHandle is the same as the SceneOriginHandle. @@ -3561,7 +3632,7 @@ internal void SceneChangedUpdate(Scene scene, bool notify = false) OnMigratedToNewScene?.Invoke(); // Only the authority side will notify clients of non-parented NetworkObject scene changes - if (isAuthority && notify && !transform.parent) + if (m_HasAuthority && notify && !transform.parent) { NetworkManagerOwner.SceneManager.NotifyNetworkObjectSceneChanged(this); } diff --git a/com.unity.netcode.gameobjects/Runtime/Logging/NetworkLog.cs b/com.unity.netcode.gameobjects/Runtime/Logging/NetworkLog.cs index 12fd9a60bb..df08bee61f 100644 --- a/com.unity.netcode.gameobjects/Runtime/Logging/NetworkLog.cs +++ b/com.unity.netcode.gameobjects/Runtime/Logging/NetworkLog.cs @@ -1,8 +1,10 @@ +using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; using System.Text.RegularExpressions; using Unity.Netcode.Logging; using UnityEngine; +using UnityEngine.Assertions; namespace Unity.Netcode { @@ -158,5 +160,11 @@ private static bool TryGetNetworkObjectName([NotNull] NetworkManager networkMana return true; } + [HideInCallstack] + [Conditional("NETCODE_INTERNAL")] + internal static void InternalAssert(bool condition, string message) + { + Assert.IsTrue(condition, message); + } } } diff --git a/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkSpawnManager.cs b/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkSpawnManager.cs index 449ca04100..38711e155c 100644 --- a/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkSpawnManager.cs +++ b/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkSpawnManager.cs @@ -1137,48 +1137,13 @@ internal bool AuthorityLocalSpawn([NotNull] NetworkObject networkObject, ulong n networkObject.NetworkManagerOwner = NetworkManager; networkObject.InvokeBehaviourNetworkPreSpawn(); - // DANGO-TODO: It would be nice to allow users to specify which clients are observers prior to spawning - // For now, this is the best place I could find to add all connected clients as observers for newly - // instantiated and spawned NetworkObjects on the authoritative side. - if (NetworkManager.DistributedAuthorityMode) + if (NetworkManager.DistributedAuthorityMode && NetworkManager.NetworkConfig.EnableSceneManagement && sceneObject) { - if (NetworkManager.NetworkConfig.EnableSceneManagement && sceneObject) - { - networkObject.SceneOriginHandle = networkObject.gameObject.scene.handle; - networkObject.NetworkSceneHandle = NetworkManager.SceneManager.ClientSceneHandleToServerSceneHandle[networkObject.gameObject.scene.handle]; - } - - // Always add the owner/authority even if SpawnWithObservers is false - // (authority should not take into consideration networkObject.CheckObjectVisibility when SpawnWithObservers is false) - if (!networkObject.SpawnWithObservers) - { - networkObject.AddObserver(ownerClientId); - } - else - { - foreach (var clientId in NetworkManager.ConnectionManager.ConnectedClientIds) - { - // If SpawnWithObservers is enabled, then authority does take networkObject.CheckObjectVisibility into consideration - if (networkObject.CheckObjectVisibility != null && !networkObject.CheckObjectVisibility.Invoke(clientId)) - { - continue; - } - networkObject.AddObserver(clientId); - } - - // Sanity check to make sure the owner is always included - // Intentionally checking as opposed to just assigning in order to generate notification. - if (!networkObject.Observers.Contains(ownerClientId)) - { - if (NetworkManager.LogLevel <= LogLevel.Error) - { - NetworkLog.LogError($"Client-{ownerClientId} is the owner of {networkObject.name} but is not an observer! Adding owner, but there is a bug in observer synchronization!"); - } - networkObject.AddObserver(ownerClientId); - } - } + networkObject.SceneOriginHandle = networkObject.gameObject.scene.handle; + networkObject.NetworkSceneHandle = NetworkManager.SceneManager.ClientSceneHandleToServerSceneHandle[networkObject.gameObject.scene.handle]; } + if (!SpawnNetworkObjectLocallyCommon(networkObject, networkId, sceneObject, playerObject, ownerClientId, destroyWithScene)) { if (NetworkManager.LogLevel <= LogLevel.Error) @@ -1295,9 +1260,10 @@ internal bool NonAuthorityLocalSpawn(in NetworkObject.SerializedObject serialize } /// - /// Handles the all the final setup and spawning needed for + /// Handles all the final setup and spawning needed for spawning a NetworkObject locally. /// - /// boolean indicating whether the spawn succeeded. Internal dev note: THIS IS A CATCH FOR OURSELVES. DON'T PULL OUT + /// boolean indicating whether the spawn succeeded. + // Internal dev note: THIS IS A CATCH FOR OURSELVES. DON'T PULL OUT internal bool SpawnNetworkObjectLocallyCommon(NetworkObject networkObject, ulong networkId, bool sceneObject, bool playerObject, ulong ownerClientId, bool destroyWithScene) { // TODO: Replace the following checks with internal Netcode asserts @@ -1311,7 +1277,7 @@ internal bool SpawnNetworkObjectLocallyCommon(NetworkObject networkObject, ulong return false; } - if (networkId == default) + if (networkId == 0) { if (NetworkManager.LogLevel <= LogLevel.Error) { @@ -1326,82 +1292,23 @@ internal bool SpawnNetworkObjectLocallyCommon(NetworkObject networkObject, ulong networkObject.SetSceneObjectStatus(sceneObject); #pragma warning restore CS0618 // Type or member is obsolete - // Always check to make sure our scene of origin is properly set for in-scene placed NetworkObjects - // Note: Always check SceneOriginHandle directly at this specific location. - if (networkObject.InScenePlaced && networkObject.SceneOriginHandle.IsEmpty()) - { - networkObject.SceneOrigin = networkObject.gameObject.scene; - } - - networkObject.NetworkObjectId = networkId; - - networkObject.DestroyWithScene = sceneObject || destroyWithScene; - - networkObject.IsPlayerObject = playerObject; + networkObject.SetupOnSpawn(networkId, playerObject, ownerClientId, destroyWithScene); - networkObject.OwnerClientId = ownerClientId; - - // When spawned, previous owner is always the first assigned owner - networkObject.PreviousOwnerId = ownerClientId; - - // If this the player and the client is the owner, then lock ownership by default - if (NetworkManager.DistributedAuthorityMode && NetworkManager.LocalClientId == ownerClientId && playerObject) - { - networkObject.AddOwnershipExtended(NetworkObject.OwnershipStatusExtended.Locked); - } - - networkObject.IsSpawned = true; SpawnedObjects.Add(networkObject.NetworkObjectId, networkObject); SpawnedObjectsList.Add(networkObject); - - // If we are not running in DA mode, this is the server, and the NetworkObject has SpawnWithObservers set, - // then add all connected clients as observers - if (!NetworkManager.DistributedAuthorityMode && NetworkManager.IsServer && networkObject.SpawnWithObservers) - { - // If running as a server only, then make sure to always add the server's client identifier - if (!NetworkManager.IsHost) - { - networkObject.AddObserver(NetworkManager.LocalClientId); - } - - // Add client observers - for (int i = 0; i < NetworkManager.ConnectedClientsIds.Count; i++) - { - // If CheckObjectVisibility has a callback, then allow that method determine who the observers are. - if (networkObject.CheckObjectVisibility != null && !networkObject.CheckObjectVisibility(NetworkManager.ConnectedClientsIds[i])) - { - continue; - } - networkObject.AddObserver(NetworkManager.ConnectedClientsIds[i]); - } - } - networkObject.ApplyNetworkParenting(); + NetworkObject.CheckOrphanChildren(); AddNetworkObjectToSceneChangedUpdates(networkObject); - + UpdateOwnershipTable(networkObject, ownerClientId); networkObject.InvokeBehaviourNetworkSpawn(); - - // Only dynamically spawned NetworkObjects are allowed - if (!sceneObject) - { - networkObject.SubscribeToActiveSceneForSynch(); - } - if (networkObject.IsPlayerObject) { UpdateNetworkClientPlayer(networkObject); } - // If we are an in-scene placed NetworkObject and our InScenePlacedSourceGlobalObjectIdHash is set - // then assign this to the PrefabGlobalObjectIdHash - if (networkObject.InScenePlaced && networkObject.InScenePlacedSourceGlobalObjectIdHash != 0) - { - networkObject.PrefabGlobalObjectIdHash = networkObject.InScenePlacedSourceGlobalObjectIdHash; - } - return true; } @@ -1554,8 +1461,7 @@ internal void ServerResetShutdownStateForSceneObjects() { continue; } - sobj.IsSpawned = false; - sobj.DestroyWithScene = false; + sobj.ResetOnDespawn(); } } diff --git a/com.unity.netcode.gameobjects/Tests/Runtime/DistributedAuthority/DistributedAuthorityCodecTests.cs b/com.unity.netcode.gameobjects/Tests/Runtime/DistributedAuthority/DistributedAuthorityCodecTests.cs index b6157324bc..fb88b9ada8 100644 --- a/com.unity.netcode.gameobjects/Tests/Runtime/DistributedAuthority/DistributedAuthorityCodecTests.cs +++ b/com.unity.netcode.gameobjects/Tests/Runtime/DistributedAuthority/DistributedAuthorityCodecTests.cs @@ -76,11 +76,6 @@ internal class TestNetworkComponent : NetworkBehaviour { public NetworkList MyNetworkList = new NetworkList(new List { 1, 2, 3 }); public NetworkVariable MyNetworkVar = new NetworkVariable(3); - - [Rpc(SendTo.Authority)] - public void TestAuthorityRpc(byte[] _) - { - } } protected override void OnOneTimeSetup() @@ -157,62 +152,6 @@ protected override IEnumerator OnStartedServerAndClients() m_Client.MessageManager.Hook(m_ClientCodecHook); } - [UnityTest] - public IEnumerator AuthorityRpc() - { - var player = m_Client.LocalClient.PlayerObject; - player.OwnerClientId = m_Client.LocalClientId + 1; - - var networkComponent = player.GetComponent(); - networkComponent.UpdateNetworkProperties(); - networkComponent.TestAuthorityRpc(new byte[] { 1, 2, 3, 4 }); - - // Universal Rpcs are sent as a ProxyMessage (which contains an RpcMessage) - yield return m_ClientCodecHook.WaitForMessageReceived(); - } - - [UnityTest] - public IEnumerator ChangeOwnership() - { - var message = new ChangeOwnershipMessage - { - DistributedAuthorityMode = true, - NetworkObjectId = 100, - OwnerClientId = 2, - }; - - yield return SendMessage(ref message); - } - - [UnityTest] - public IEnumerator ClientConnected() - { - var message = new ClientConnectedMessage() - { - ClientId = 2, - }; - - yield return SendMessage(ref message); - } - - [UnityTest] - public IEnumerator ClientDisconnected() - { - var message = new ClientDisconnectedMessage() - { - ClientId = 2, - }; - - yield return SendMessage(ref message); - } - - [UnityTest] - public IEnumerator CreateObject() - { - SpawnObject(m_SpawnObject, m_Client); - yield return m_ClientCodecHook.WaitForMessageReceived(); - } - [UnityTest] public IEnumerator DestroyObject() { diff --git a/com.unity.netcode.gameobjects/Tests/Runtime/NetworkObject/NetworkObjectOnSpawnTests.cs b/com.unity.netcode.gameobjects/Tests/Runtime/NetworkObject/NetworkObjectOnSpawnTests.cs index 0cdb31c11e..5b5583f31d 100644 --- a/com.unity.netcode.gameobjects/Tests/Runtime/NetworkObject/NetworkObjectOnSpawnTests.cs +++ b/com.unity.netcode.gameobjects/Tests/Runtime/NetworkObject/NetworkObjectOnSpawnTests.cs @@ -341,7 +341,17 @@ bool HasConditionBeenMet() Assert.False(s_GlobalTimeoutHelper.TimedOut, "Timed out while waiting for client side despawns!"); //----------- step 2 check spawn and destroy again - authorityInstance.GetComponent().Spawn(); + var authorityObject = authorityInstance.GetComponent(); + authorityObject.Spawn(); + + // This validates invoking GetSceneOriginHandle will not throw an exception for a dynamically spawned NetworkObject + // when the scene of origin hasn't been set. + var sceneOriginHandle = authorityObject.GetSceneOriginHandle(); + + // This validates that GetSceneOriginHandle will return the GameObject's scene handle that should be the currently active scene + var activeSceneHandle = SceneManager.GetActiveScene().handle; + Assert.IsTrue(sceneOriginHandle == activeSceneHandle, $"{nameof(NetworkObject)} should have returned the active scene handle of {activeSceneHandle} but returned {sceneOriginHandle}"); + // wait a tick yield return s_DefaultWaitForTick; // check spawned again on server this is 2 because we are reusing the object which was already spawned once. @@ -366,22 +376,6 @@ bool HasConditionBeenMet() Assert.False(s_GlobalTimeoutHelper.TimedOut, "Timed out while waiting for client side despawns! (2nd pass)"); } - [Test] - public void DynamicallySpawnedNoSceneOriginException() - { - var gameObject = new GameObject(); - var networkObject = gameObject.AddComponent(); - networkObject.IsSpawned = true; - networkObject.SceneOriginHandle = default; - // This validates invoking GetSceneOriginHandle will not throw an exception for a dynamically spawned NetworkObject - // when the scene of origin hasn't been set. - var sceneOriginHandle = networkObject.GetSceneOriginHandle(); - - // This validates that GetSceneOriginHandle will return the GameObject's scene handle that should be the currently active scene - var activeSceneHandle = SceneManager.GetActiveScene().handle; - Assert.IsTrue(sceneOriginHandle == activeSceneHandle, $"{nameof(NetworkObject)} should have returned the active scene handle of {activeSceneHandle} but returned {sceneOriginHandle}"); - } - private class TrackOnSpawnFunctions : NetworkBehaviour { public int OnNetworkSpawnCalledCount { get; private set; } diff --git a/testproject/ProjectSettings/ProjectSettings.asset b/testproject/ProjectSettings/ProjectSettings.asset index 176a6e2232..61fc950e7c 100644 --- a/testproject/ProjectSettings/ProjectSettings.asset +++ b/testproject/ProjectSettings/ProjectSettings.asset @@ -739,7 +739,7 @@ PlayerSettings: webWasm2023: 0 webEnableSubmoduleStrippingCompatibility: 0 scriptingDefineSymbols: - Standalone: UNITY_NETCODE_NATIVE_COLLECTION_SUPPORT + Standalone: UNITY_NETCODE_NATIVE_COLLECTION_SUPPORT;NETCODE_INTERNAL additionalCompilerArguments: Standalone: [] platformArchitecture: {} From 780080303343742052a5cd29c3d15259d9911673 Mon Sep 17 00:00:00 2001 From: Noel Stephens Date: Wed, 1 Jul 2026 08:08:59 -0500 Subject: [PATCH 20/26] fix: fastbufferreader string deserialization int overflow (#4052) * fix This fixes an edge case scenario with string reading where if user code has caused the character count to have been already read prior to attempting to read a string value (safely or unsafely) and then reading the string can result in the signed integer size to roll over to a negative value and thus causing the reader to attempt to read into restricted memory outside of the application domain which results in the editor crashing. The fix catches this scenario and throws an overflow exception prior to attempting to read into negative memory space relative to the application domain. * test This test validates the fix. * update Adding changelog entry. * update Making the reader use the already calculated readSize. * test Updating test to unsafely read a string under the same condition. * fix Fixing an issue where the unsafe string read needs to create an empty string based on the character count and not the byte count. * refactor Based on Paolo's suggestion on combining the string size, in bytes, validation script to a single in-lined method shared between the safe and unsafe string read methods. Also combined the actual reading of the string data into a single in-lined method. * style Removing the auto-added (and not used) UnityEngine.UIElements using directive. * style updated comments and renamed CheckIfValidStringLength to ValidateStringByteCount. * test Removing the added 3 bytes used for earlier debugging purposes. * style spelling/typo fix. whitespace after sentence in comment fix. * refactor Based on Emma's suggestion, removing the `SizeOfLengthField` method and `TryBeginReadInternal` check within the ReadValueSafe (string) method and replacing that with `ReadLengthSafe`. * Update com.unity.netcode.gameobjects/Runtime/Serialization/FastBufferReader.cs Co-authored-by: Emma * update Based on suggestion from Emma, removing InBitwiseContext check since this is done in both ReadLengthSafe and TryBeginReadInternal. --------- Co-authored-by: Emma --- com.unity.netcode.gameobjects/CHANGELOG.md | 2 + .../Runtime/Serialization/FastBufferReader.cs | 108 ++++++++++-------- .../Serialization/FastBufferReaderTests.cs | 44 +++++++ 3 files changed, 108 insertions(+), 46 deletions(-) diff --git a/com.unity.netcode.gameobjects/CHANGELOG.md b/com.unity.netcode.gameobjects/CHANGELOG.md index 3f6df83f11..0c5e47d1a0 100644 --- a/com.unity.netcode.gameobjects/CHANGELOG.md +++ b/com.unity.netcode.gameobjects/CHANGELOG.md @@ -21,6 +21,8 @@ Additional documentation and release notes are available at [Multiplayer Documen ### Fixed + +- Issue when FastBufferReader is attempting to read a string and all or a portion of the character count has already been read by user script, it could read a character length that results in a negative byte length which could result in an editor crash. (#4052) - Issue where NetworkRigidbodyBase was not applying rotation correctly when using Rigidbody2D. (#4012) - Issue where NetworkRigidbodyBase was always checking the 3D rigid body's interpolation mode when determining if it is kinematic and needs to put the rigid body to sleep and then switch to interpolation. (#4012) diff --git a/com.unity.netcode.gameobjects/Runtime/Serialization/FastBufferReader.cs b/com.unity.netcode.gameobjects/Runtime/Serialization/FastBufferReader.cs index d420ccd28e..d132f775fd 100644 --- a/com.unity.netcode.gameobjects/Runtime/Serialization/FastBufferReader.cs +++ b/com.unity.netcode.gameobjects/Runtime/Serialization/FastBufferReader.cs @@ -564,14 +564,36 @@ public void ReadNetworkSerializableInPlace(ref T value) where T : INetworkSer } /// - /// Reads a string - /// NOTE: ALLOCATES + /// Validates the string's total byte count based on whether we are + /// using one or two byte characters. + /// + /// + /// Will throw an overflow exception if the size is greater than . + /// + /// Character count + /// If false(default) 2 byte characters and if true 1 byte characters + /// total size in bytes to read + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private int ValidateStringByteCount(int length, bool oneByteChars) + { + var readSize = oneByteChars ? length : length * sizeof(char); + if (int.MaxValue < (uint)readSize) + { + throw new OverflowException($"Invalid reader position detected when trying to read a string of size {(uint)readSize}! This can result from an error in the serialization. Ensure deserialization exactly matches what was serialized!"); + } + return readSize; + } + + /// + /// Commonly shared string read method between . /// - /// Stores the read string - /// Whether or not to use one byte per character. This will only allow ASCII - public unsafe void ReadValue(out string s, bool oneByteChars = false) + /// The output of the string read. + /// The number of characters in the string. + /// If false(default) 2 byte characters and if true 1 byte characters. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private unsafe void ReadString(out string s, int length, bool oneByteChars) { - ReadLength(out int length); s = "".PadRight(length); int target = s.Length; fixed (char* native = s) @@ -592,56 +614,50 @@ public unsafe void ReadValue(out string s, bool oneByteChars = false) } /// - /// Reads a string. - /// NOTE: ALLOCATES - /// - /// "Safe" version - automatically performs bounds checking. Less efficient than bounds checking - /// for multiple reads at once by calling TryBeginRead. + /// Reads a string without bounds checking. + /// NOTE: This method ALLOCATES memory. /// + /// + /// This is the un-safe string read which requires invoking prior to invoking this method.
+ /// Using one byte characters only allows ASCII characters. + ///
/// Stores the read string - /// Whether or not to use one byte per character. This will only allow ASCII - public unsafe void ReadValueSafe(out string s, bool oneByteChars = false) + /// If false(default) 2 byte characters and if true 1 byte characters. + public unsafe void ReadValue(out string s, bool oneByteChars = false) { -#if DEBUG - if (Handle->InBitwiseContext) - { - throw new InvalidOperationException( - "Cannot use BufferReader in bytewise mode while in a bitwise context."); - } -#endif + ReadLength(out int length); - if (!TryBeginReadInternal(SizeOfLengthField())) - { - throw new OverflowException("Reading past the end of the buffer"); - } + // Validate the string's byte count based on the character count. + ValidateStringByteCount(length, oneByteChars); - ReadLength(out int length); + // Read the string + ReadString(out s, length, oneByteChars); + } + + /// + /// Reads a string after it performs bounds checking automatically. + /// NOTE: This method ALLOCATES memory. + /// + /// + /// This is the safe string read which invokes prior to reading the string.
+ /// Using one byte characters only allows ASCII characters. + ///
+ /// The string re the read string + /// If false(default) 2 byte characters and if true 1 byte characters. + public unsafe void ReadValueSafe(out string s, bool oneByteChars = false) + { + ReadLengthSafe(out int length); - if (!TryBeginReadInternal(length * (oneByteChars ? 1 : sizeof(char)))) + // Validate the string's byte count based on the character count and if it is valid begin reading based on the returned + // byte count. + if (!TryBeginReadInternal(ValidateStringByteCount(length, oneByteChars))) { throw new OverflowException("Reading past the end of the buffer"); } - s = "".PadRight(length); - int target = s.Length; - fixed (char* native = s) - { - if (oneByteChars) - { - for (int i = 0; i < target; ++i) - { - ReadByte(out byte b); - native[i] = (char)b; - } - } - else - { - ReadBytes((byte*)native, target * sizeof(char)); - } - } - } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static int SizeOfLengthField() => sizeof(uint); + // Read the string + ReadString(out s, length, oneByteChars); + } [MethodImpl(MethodImplOptions.AggressiveInlining)] private void ReadLengthSafe(out uint length) => ReadUnmanagedSafe(out length); diff --git a/com.unity.netcode.gameobjects/Tests/Editor/Serialization/FastBufferReaderTests.cs b/com.unity.netcode.gameobjects/Tests/Editor/Serialization/FastBufferReaderTests.cs index 85aabbb52e..d38c10e0a7 100644 --- a/com.unity.netcode.gameobjects/Tests/Editor/Serialization/FastBufferReaderTests.cs +++ b/com.unity.netcode.gameobjects/Tests/Editor/Serialization/FastBufferReaderTests.cs @@ -924,6 +924,50 @@ public void GivenFastBufferWriterContainingValue_WhenReadingString_ValueMatchesW } } + /// + /// This validates that catches a potential + /// scenario where the string's character count value has already been read + /// due to an error within user script and the resultant character count + /// multiplied times 2 (when using 2 bytes vs 1) causes the length to roll over + /// to a negative value which, in turn, causes the reader to attempt to read + /// into restricted memory and causes the editor to crash. + /// + [Test] + public void ReadingStringAfterStringLengthHasAlreadyBeenRead([Values] bool isSafeRead) + { + // This was an issue uncovered in UUM-145752 that resulted + // in the below text to result in a length that when using + // 2 bytes per character would cause the skewed size to roll + // over into a negative value causing the editor to crash + // when it attempted to read a large negative offset value. + string valueToTest = "true"; + + var serializedValueSize = FastBufferWriter.GetWriteSize(valueToTest); + + using var writer = new FastBufferWriter(serializedValueSize, Allocator.Temp); + writer.WriteValueSafe(valueToTest); + + using var reader = new FastBufferReader(writer, Allocator.Temp); + + // Read the value of the character count before trying to read the string + // This mocks user code having read too far into a stream causing the position to be skewed such that + // the string reader reads the some of the bytes for the actual text as the length. + reader.ReadByteSafe(out byte count); + Assert.True(count == valueToTest.Length, $"Count ({count}) is not the expected size of {valueToTest.Length}!"); + if (isSafeRead) + { + // This should throw an overflow exception but should not crash the editor. + Assert.Throws(() => reader.ReadValueSafe(out string valueRead)); + } + else + { + // Assume user does a pre-calculation of the size to be read: + Assert.IsTrue(reader.TryBeginRead(count), "Reader denied read permission"); + + // This should throw an overflow exception but should not crash the editor. + Assert.Throws(() => reader.ReadValue(out string valueRead)); + } + } [TestCase(1, 0)] [TestCase(2, 0)] From 10e01eb7222d6b5804201721694e18737e6ce71a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Chrobot?= <124174716+michalChrobot@users.noreply.github.com> Date: Sat, 4 Jul 2026 13:32:54 +0200 Subject: [PATCH 21/26] fix: remove dead API doc links to internal scene event types (#4064) fix: remove dead API doc links to internal scene event types (UUM-131558) The NetworkSceneManager class summary and SceneEventDelegate XML docs referenced the internal types SceneEventMessage and SceneEventData via , which generate hyperlinks in the DocFX-generated API docs. Because those types are internal they have no public documentation pages, so the links resolved to missing pages. Convert the descriptive class-summary references to inline code () and drop the internal SceneEventData entry from the SceneEventDelegate 'See also' list (a user cannot reference an internal type), keeping the public SceneEvent link. --- .../Runtime/SceneManagement/NetworkSceneManager.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/com.unity.netcode.gameobjects/Runtime/SceneManagement/NetworkSceneManager.cs b/com.unity.netcode.gameobjects/Runtime/SceneManagement/NetworkSceneManager.cs index 8c07fbd603..a28baf1f94 100644 --- a/com.unity.netcode.gameobjects/Runtime/SceneManagement/NetworkSceneManager.cs +++ b/com.unity.netcode.gameobjects/Runtime/SceneManagement/NetworkSceneManager.cs @@ -143,7 +143,7 @@ public class SceneEvent /// /// Main class for managing network scenes when is enabled. - /// Uses the message to communicate between the server and client(s) + /// Uses the SceneEventMessage message to communicate SceneEventData between the server and client(s) /// [Serializable] public class NetworkSceneManager : IDisposable @@ -162,8 +162,7 @@ public class NetworkSceneManager : IDisposable /// /// The delegate callback definition for scene event notifications.
/// See also:
- ///
- /// + /// ///
/// SceneEvent which contains information about the scene event, including type, progress, and scene details public delegate void SceneEventDelegate(SceneEvent sceneEvent); From 0dce182b1ab312b90f244c7e12c45719d67d3ff3 Mon Sep 17 00:00:00 2001 From: Emma Date: Mon, 6 Jul 2026 19:03:27 -0400 Subject: [PATCH 22/26] fix: Serialization docs (#4007) * fix: Serialization docs * Apply suggestions from code review Co-authored-by: Amy Reeve * Apply suggestion from @jabbacakes Co-authored-by: Amy Reeve * small changes * Fix pvp exceptions * Fix pvp errors * Fix formatting * Use RuntimeInitializeOnLoad * fix the tests * Fix tests v2 electric boogaloo * Add details in README files --------- Co-authored-by: Amy Reeve --- .../Documentation~/TableOfContents.md | 10 +- .../advanced-topics/bufferserializer.md | 7 + .../advanced-topics/custom-serialization.md | 114 ++++----- .../fastbufferwriter-fastbufferreader.md | 7 + .../advanced-topics/message-system/rpc.md | 1 + .../serialization/inetworkserializable.md | 45 ++-- .../inetworkserializebymemcpy.md | 7 +- .../networkobject-serialization.md | 18 +- .../serialization/serialization-arrays.md | 40 +-- .../serialization/serialization-overview.md | 36 +++ .../basics/custom-networkvariables.md | 179 ++----------- .../Documentation~/basics/networkvariable.md | 92 +------ .../Documentation~/serialization.md | 7 +- .../TypedSerializerImplementations.cs | 30 +++ .../Editor/DocumentationCodeSamples.meta | 3 + .../Editor/DocumentationCodeSamples/README.md | 31 +++ .../DocumentationCodeSamples/README.md.meta | 7 + .../Serialization.meta | 3 + .../SerializationCustomization.cs | 114 +++++++++ .../SerializationCustomization.cs.meta | 3 + .../Runtime/DocumentationCodeSamples.meta | 8 + .../NetworkVariable.meta | 8 + .../NetworkVariable/CustomNetworkVariable.cs | 239 ++++++++++++++++++ .../CustomNetworkVariable.cs.meta | 2 + .../CustomSerializationDocsTests.cs | 213 ++++++++++++++++ .../CustomSerializationDocsTests.cs.meta | 3 + .../DocumentationCodeSamples/README.md | 33 +++ .../DocumentationCodeSamples/README.md.meta | 7 + 28 files changed, 887 insertions(+), 380 deletions(-) create mode 100644 com.unity.netcode.gameobjects/Documentation~/advanced-topics/serialization/serialization-overview.md create mode 100644 com.unity.netcode.gameobjects/Tests/Editor/DocumentationCodeSamples.meta create mode 100644 com.unity.netcode.gameobjects/Tests/Editor/DocumentationCodeSamples/README.md create mode 100644 com.unity.netcode.gameobjects/Tests/Editor/DocumentationCodeSamples/README.md.meta create mode 100644 com.unity.netcode.gameobjects/Tests/Editor/DocumentationCodeSamples/Serialization.meta create mode 100644 com.unity.netcode.gameobjects/Tests/Editor/DocumentationCodeSamples/Serialization/SerializationCustomization.cs create mode 100644 com.unity.netcode.gameobjects/Tests/Editor/DocumentationCodeSamples/Serialization/SerializationCustomization.cs.meta create mode 100644 com.unity.netcode.gameobjects/Tests/Runtime/DocumentationCodeSamples.meta create mode 100644 com.unity.netcode.gameobjects/Tests/Runtime/DocumentationCodeSamples/NetworkVariable.meta create mode 100644 com.unity.netcode.gameobjects/Tests/Runtime/DocumentationCodeSamples/NetworkVariable/CustomNetworkVariable.cs create mode 100644 com.unity.netcode.gameobjects/Tests/Runtime/DocumentationCodeSamples/NetworkVariable/CustomNetworkVariable.cs.meta create mode 100644 com.unity.netcode.gameobjects/Tests/Runtime/DocumentationCodeSamples/NetworkVariable/CustomSerializationDocsTests.cs create mode 100644 com.unity.netcode.gameobjects/Tests/Runtime/DocumentationCodeSamples/NetworkVariable/CustomSerializationDocsTests.cs.meta create mode 100644 com.unity.netcode.gameobjects/Tests/Runtime/DocumentationCodeSamples/README.md create mode 100644 com.unity.netcode.gameobjects/Tests/Runtime/DocumentationCodeSamples/README.md.meta diff --git a/com.unity.netcode.gameobjects/Documentation~/TableOfContents.md b/com.unity.netcode.gameobjects/Documentation~/TableOfContents.md index e78e7192f6..47bd50c6bc 100644 --- a/com.unity.netcode.gameobjects/Documentation~/TableOfContents.md +++ b/com.unity.netcode.gameobjects/Documentation~/TableOfContents.md @@ -70,15 +70,17 @@ * [Network update loop reference](advanced-topics/network-update-loop-system/network-update-loop-reference.md) * [Network time and ticks](advanced-topics/networktime-ticks.md) * [Serialization](serialization.md) + * [Serialization overview](advanced-topics/serialization/serialization-overview.md) * [C# primitives](advanced-topics/serialization/cprimitives.md) * [Unity primitives](advanced-topics/serialization/unity-primitives.md) * [Enum types](advanced-topics/serialization/enum-types.md) * [Arrays](advanced-topics/serialization/serialization-arrays.md) - * [INetworkSerializable](advanced-topics/serialization/inetworkserializable.md) - * [INetworkSerializeByMemcpy](advanced-topics/serialization/inetworkserializebymemcpy.md) - * [Custom serialization](advanced-topics/custom-serialization.md) - * [NetworkObject serialization](advanced-topics/serialization/networkobject-serialization.md) + * [Customize serializable types with INetworkSerializable](advanced-topics/serialization/inetworkserializable.md) + * [Serialize unmanaged structs with INetworkSerializeByMemcpy](advanced-topics/serialization/inetworkserializebymemcpy.md) + * [NetworkObject and NetworkBehaviour serialization](advanced-topics/serialization/networkobject-serialization.md) * [FastBufferWriter and FastBufferReader](advanced-topics/fastbufferwriter-fastbufferreader.md) + * [BufferSerializer](advanced-topics/bufferserializer.md) + * [Custom serialization](advanced-topics/custom-serialization.md) * [Scene management](scene-management.md) * [Scene management overview](basics/scenemanagement/scene-management-overview.md) * [Integrated management](integrated-management.md) diff --git a/com.unity.netcode.gameobjects/Documentation~/advanced-topics/bufferserializer.md b/com.unity.netcode.gameobjects/Documentation~/advanced-topics/bufferserializer.md index 22375bccf5..0f5df51f15 100644 --- a/com.unity.netcode.gameobjects/Documentation~/advanced-topics/bufferserializer.md +++ b/com.unity.netcode.gameobjects/Documentation~/advanced-topics/bufferserializer.md @@ -1,5 +1,8 @@ # BufferSerializer +> [!NOTE] +> Read the [Serialization overview](./serialization/serialization-overview.md) page to understand the basics of serialization before learning how to use `BufferSerializer`. + `BufferSerializer` is the bi-directional serializer primarily used for serializing within [`INetworkSerializable`](serialization/inetworkserializable.md) types. It wraps [`FastBufferWriter` and `FastBufferReader`](fastbufferwriter-fastbufferreader.md) to provide high performance serialization, but has a couple of differences to make it more user-friendly: - Rather than writing separate methods for serializing and deserializing, `BufferSerializer` allows writing a single method that can handle both operations, which reduces the possibility of a mismatch between the two @@ -15,3 +18,7 @@ However, when those downsides are unreasonable, `BufferSerializer - For performance, you can use `PreCheck(int amount)` followed by `SerializeValuePreChecked()` to perform bounds checking for multiple fields at once. - For both performance and bandwidth usage, you can obtain the wrapped underlying reader/writer via `serializer.GetFastBufferReader()` when `serializer.IsReader` is `true`, and `serializer.GetFastBufferWriter()` when `serializer.IsWriter` is `true`. These provide micro-performance improvements by removing a level of indirection, and also give you a type you can use with `BytePacker` and `ByteUnpacker`. + +## Serializing custom types + +`BufferSerializer` can be extended via extension methods to handle serializing custom types. Refer to [customizing `BufferSerializer`](./custom-serialization.md#bufferserializer) for instructions on how to do this. diff --git a/com.unity.netcode.gameobjects/Documentation~/advanced-topics/custom-serialization.md b/com.unity.netcode.gameobjects/Documentation~/advanced-topics/custom-serialization.md index 7a04e2f123..b7b23e4afc 100644 --- a/com.unity.netcode.gameobjects/Documentation~/advanced-topics/custom-serialization.md +++ b/com.unity.netcode.gameobjects/Documentation~/advanced-topics/custom-serialization.md @@ -1,93 +1,71 @@ # Custom serialization -Netcode uses a default serialization pipeline when using `RPC`s, `NetworkVariable`s, or any other Netcode-related tasks that require serialization. The serialization pipeline looks like this: +> [!NOTE] +> Read the [Serialization overview](./serialization/serialization-overview.md) page to understand the basics of serialization before learning how to customize serialization. -`` -Custom Types => Built In Types => INetworkSerializable -`` +Netcode for GameObjects provide support for serializing any unsupported types, and with the API provided, it can even be done with types that you haven't defined yourself, those who are behind a 3rd party wall, such as .NET types. However, the way custom serialization is implemented for RPCs and NetworkVariables is slightly different. -That is, when Netcode first gets hold of a type, it will check for any custom types that the user have registered for serialization, after that it will check if it's a built in type, such as a Vector3, float etc. These are handled by default. If not, it will check if the type inherits `INetworkSerializable`, if it does, it will call it's write methods. +Netcode for GameObjects supports custom serialization of unsupported types, including those you haven't defined yourself, such as third-party .NET types. You can also use custom serialization to override how an existing supported type is serialized. -By default, any type that satisfies the `unmanaged` generic constraint can be automatically serialized as RPC parameters. This includes all basic types (bool, byte, int, float, enum, etc) as well as any structs that has only these basic types. +Custom serialization is implemented slightly differently for RPCs and NetworkVariables. The examples on this page provide different ways to serialization a custom health struct. -With this flow, you can provide support for serializing any unsupported types, and with the API provided, it can even be done with types that you haven't defined yourself, those who are behind a 3rd party wall, such as .NET types. However, the way custom serialization is implemented for RPCs and NetworkVariables is slightly different. +[!code-cs[](../../Tests/Editor/DocumentationCodeSamples/Serialization/SerializationCustomization.cs#HealthStruct)] -### Serialize a type in a Remote Procedure Call (RPC) +## FastBufferReader and FastBufferWriter -> [!NOTE] -> From versioln 1.7.0 Remote Procedure Calls (RPCs) can also use the Network Variable flow, but NetworkVariables can't use the RPC flow. The RPC flow is more efficient when RPCs serialize the type. Unity selects the RPC flow if you implement both the RPC and Network variable flows. When a type is used by both NetworkVariables and RPCs you can use the NetworkVariable flow to lower maintenance requirements. +[`FastBufferReader` and `FastBufferWriter`](./fastbufferwriter-fastbufferreader.md) are the main serialization tools in Netcode for GameObjects. To register serialization for a custom type, or override an already handled type, you need to create extension methods for `FastBufferReader.ReadValueSafe()` and `FastBufferWriter.WriteValueSafe()`. `FastBufferReader` and `FastBufferWriter` can read and write primitive types, and you can extend this functionality to serialize your custom type. -To register a custom type, or override an already handled type, you need to create extension methods for `FastBufferReader.ReadValueSafe()` and `FastBufferWriter.WriteValueSafe()`: +[!code-cs[](../../Tests/Editor/DocumentationCodeSamples/Serialization/SerializationCustomization.cs#FastBuffer)] -```csharp -// Tells the Netcode how to serialize and deserialize Url in the future. -// The class name doesn't matter here. -public static class SerializationExtensions -{ - public static void ReadValueSafe(this FastBufferReader reader, out Url url) - { - reader.ReadValueSafe(out string val); - url = new Url(val); - } - - public static void WriteValueSafe(this FastBufferWriter writer, in Url url) - { - writer.WriteValueSafe(url.Value); - } -} -``` +You may also need to add extensions for `FastBufferReader.ReadValue()`, `FastBufferWriter.WriteValue()` if you want to serialize without [bounds checking](./fastbufferwriter-fastbufferreader.md#bounds-checking) -The code generation for RPCs will automatically pick up and use these functions, and they'll become available via `FastBufferWriter` and `FastBufferReader` directly. +## BufferSerializer -You can also optionally use the same method to add support for `BufferSerializer.SerializeValue()`, if you wish, which will make this type readily available within [`INetworkSerializable`](serialization/inetworkserializable.md) types: +You can also add custom serialization support to the bi-directional [`BufferSerializer`](./bufferserializer.md). This makes your custom type readily available within [`INetworkSerializable`](serialization/inetworkserializable.md) types and in the [`NetworkBehaviour.OnSynchronize()` method](../components/core/networkbehaviour-synchronize.md#prespawn-synchronization-with-onsynchronize): -```csharp -// The class name doesn't matter here. -public static class SerializationExtensions -{ - public static void SerializeValue(this BufferSerializer serializer, ref Url url) where TReaderWriter: IReaderWriter - { - if (serializer.IsReader) - { - url = new Url(); - } - serializer.SerializeValue(ref url.Value); - } -} -``` +[!code-cs[](../../Tests/Editor/DocumentationCodeSamples/Serialization/SerializationCustomization.cs#BufferSerializer)] -Additionally, you can also add extensions for `FastBufferReader.ReadValue()`, `FastBufferWriter.WriteValue()`, and `BufferSerializer.SerializeValuePreChecked()` to provide more optimal implementations for manual serialization using `FastBufferReader.TryBeginRead()`, `FastBufferWriter.TryBeginWrite()`, and `BufferSerializer.PreCheck()`, respectively. However, none of these will be used for serializing RPCs - only `ReadValueSafe` and `WriteValueSafe` are used. +## Remote procedure call (RPCs) -### For NetworkVariable +> [!NOTE] +> RPCs can use the Network Variable flow, but NetworkVariables can't use the RPC flow. The RPC flow is more efficient when only RPCs need to serialize the type. When a type is used by both NetworkVariables and RPCs, you can implement just the NetworkVariable flow to lower maintenance requirements. Unity will select the RPC flow for RPCs if you have implemented both flows. -`NetworkVariable` goes through a slightly different pipeline than `RPC`s and relies on a different process for determining how to serialize its types. As a result, making a custom type available to the `RPC` pipeline doesn't automatically make it available to the `NetworkVariable` pipeline, and vice-versa. The same method can be used for both, but currently, `NetworkVariable` requires an additional runtime step to make it aware of the methods. +To serialize a custom type, or override an already handled type, you need to create extension methods for `FastBufferReader.ReadValueSafe()` and `FastBufferWriter.WriteValueSafe()` as [outlined above](#fastbufferreader-and-fastbufferwriter). -To add custom serialization support in `NetworkVariable`, follow the steps from the "For RPCs" section to write extension methods for `FastBufferReader` and `FastBufferWriter`; then, somewhere in your application startup (before any `NetworkVariable`s using the affected types will be serialized) add the following: +The code generation for RPCs will automatically pick up and use these functions, as they'll become available via `FastBufferWriter` and `FastBufferReader` directly. -```csharp -UserNetworkVariableSerialization.WriteValue = SerializationExtensions.WriteValueSafe; -UserNetworkVariableSerialization.ReadValue = SerializationExtensions.ReadValueSafe; -``` +## NetworkVariable + +Implementing [`INetworkSerializable`](./serialization/inetworkserializable.md) is the cleanest and most straightforward way to customize the serialization of a type within a [`NetworkVariable`](../basics/networkvariable.md). `UserNetworkVariableSerialization` provides runtime configuration to further override serialization of a type. -You can also use lambda expressions here: +First you will need to create extension methods for `FastBufferReader.ReadValueSafe()` and `FastBufferWriter.WriteValueSafe()` as [outlined above](#fastbufferreader-and-fastbufferwriter). + +Secondly, somewhere in your application startup (before any `NetworkVariable`s using the affected types will be serialized), add the following: ```csharp -UserNetworkVariableSerialization.WriteValue = (FastBufferWriter writer, in Url url) => -{ - writer.WriteValueSafe(url.Value); -}; - -UserNetworkVariableSerialization.ReadValue = (FastBufferReader reader, out Url url) -{ - reader.ReadValueSafe(out string val); - url = new Url(val); -}; +UserNetworkVariableSerialization.WriteValue = SerializationExtensions.WriteValueSafe; +UserNetworkVariableSerialization.ReadValue = SerializationExtensions.ReadValueSafe; +UserNetworkVariableSerialization.DuplicateValue = (in Health value, ref Health duplicatedValue) => duplicatedValue = value; ``` -When you create an extension method in `NetworkVariable` you need to implement the following values: +`DuplicateValue` should return a complete deep copy of the value that `NetworkVariable` compares to a previous value, which is used to check whether the value has changed. `DuplicateValue` avoids re-serializing it over the network every frame when it hasn't changed. + +> [!NOTE] +> `WriteValue`, `ReadValue` and `DuplicateValue` all need to be defined to customize your serialization. + +> [!NOTE] +> `WriteValue` and `ReadValue` will not be used if a type implements `INetworkSerializable` or [`INetworkSerializeByMemcpy`](./serialization/inetworkserializebymemcpy.md). + +### Serializing delta updates + +Reading and writing a value provides the minimal amount of `NetworkVariable` functionality. This will synchronize your whole type whenever any value within the type value changes. To support sending delta updates rather than a full update whenever your type has changed, implement the following functions: + +- `WriteDelta` +- `ReadDelta` + +> [!NOTE] +> Both `WriteDelta` and `ReadDelta` need to be defined for either to be used. -- `WriteValue` -- `ReadValue` -- `DuplicateValue` +Here is a full implementation of a custom type with the methods needed for `UserNetworkVariableSerialization` -`DuplicateValue` returns a complete deep copy of the value that `NetworkVariable` compares to a previous value to check whether or not that values has changed. This avoids reserializing it over the network every frame when it hasn't changed. +[!code-cs[](../../Tests/Runtime/DocumentationCodeSamples/NetworkVariable/NetworkVariableSerialization.cs#HealthExample)] diff --git a/com.unity.netcode.gameobjects/Documentation~/advanced-topics/fastbufferwriter-fastbufferreader.md b/com.unity.netcode.gameobjects/Documentation~/advanced-topics/fastbufferwriter-fastbufferreader.md index 1ac9b0f9d3..3f8327fc17 100644 --- a/com.unity.netcode.gameobjects/Documentation~/advanced-topics/fastbufferwriter-fastbufferreader.md +++ b/com.unity.netcode.gameobjects/Documentation~/advanced-topics/fastbufferwriter-fastbufferreader.md @@ -1,5 +1,8 @@ # FastBufferWriter and FastBufferReader +> [!NOTE] +> Read the [Serialization overview](./serialization/serialization-overview.md) page to understand the basics of serialization before learning about `FastBufferWriter` and `FastBufferReader`. + The serialization and deserialization is done via `FastBufferWriter` and `FastBufferReader`. These have methods for serializing individual types and methods for serializing packed numbers, but in particular provide a high-performance method called `WriteValue()/ReadValue()` (for Writers and Readers, respectively) that can extremely quickly write an entire unmanaged struct to a buffer. There's a trade-off of CPU usage vs bandwidth in using this: Writing individual fields is slower (especially when it includes operations on unaligned memory), but allows the buffer to be filled more efficiently, both because it avoids padding for alignment in structs, and because it allows you to use `BytePacker.WriteValuePacked()`/`ByteUnpacker.ReadValuePacked()` and `BytePacker.WriteValueBitPacked()`/`ByteUnpacker.ReadValueBitPacked()`. The difference between these two is that the BitPacked variants pack more efficiently, but they reduce the valid range of values. See the section below for details on packing. @@ -156,3 +159,7 @@ Packing values is done using the utility classes `BytePacker` and `ByteUnpacker` | uint | 30 bits (0 to 1,073,741,824) | | long | 60 bits + sign bit (-1,152,921,504,606,846,976 to 1,152,921,504,606,846,975) | | ulong | 61 bits (0 to 2,305,843,009,213,693,952) | + +## Serializing custom types + +`FastBufferReader` and `FastBufferWriter` can be extended via extension methods to handle serializing custom types. Refer to [customizing `FastBufferReader` and `FastBufferWriter`](./custom-serialization.md#fastbufferreader-and-fastbufferwriter) for instructions on how to do this. diff --git a/com.unity.netcode.gameobjects/Documentation~/advanced-topics/message-system/rpc.md b/com.unity.netcode.gameobjects/Documentation~/advanced-topics/message-system/rpc.md index 1fb5c3dcc4..b46a1b8b91 100644 --- a/com.unity.netcode.gameobjects/Documentation~/advanced-topics/message-system/rpc.md +++ b/com.unity.netcode.gameobjects/Documentation~/advanced-topics/message-system/rpc.md @@ -328,3 +328,4 @@ void Update() ## Additional resources * [RPC parameters](rpc-params.md) +* [Customizing serialization](../custom-serialization.md#remote-procedure-call-rpc) diff --git a/com.unity.netcode.gameobjects/Documentation~/advanced-topics/serialization/inetworkserializable.md b/com.unity.netcode.gameobjects/Documentation~/advanced-topics/serialization/inetworkserializable.md index 36d83416ba..2733a0e373 100644 --- a/com.unity.netcode.gameobjects/Documentation~/advanced-topics/serialization/inetworkserializable.md +++ b/com.unity.netcode.gameobjects/Documentation~/advanced-topics/serialization/inetworkserializable.md @@ -1,9 +1,14 @@ -# INetworkSerializable +# Customize serializable types with INetworkSerializable -You can use the `INetworkSerializable` interface to define custom serializable types. +> [!NOTE] +> Read the [Serialization overview](./serialization/serialization-overview.md) page to understand the basics of serialization before customizing serializable types with `INetworkSerializable`. + +You can use the `INetworkSerializable` interface to define custom serializable types. This interface has one function: `NetworkSerialize(BufferSerializer serializer)`, which ingests a bi-directional [`BufferSerializer`](../bufferserializer.md) that you can use to implement bi-directional custom serialization. + +`INetworkSerializable` can be implemented on both managed and unmanaged types, although serializing managed types isn't recommended because it can reduce your game's performance. ```csharp -struct MyComplexStruct : INetworkSerializable +struct SpawnPoint : INetworkSerializable { public Vector3 Position; public Quaternion Rotation; @@ -18,24 +23,20 @@ struct MyComplexStruct : INetworkSerializable } ``` -Types implementing `INetworkSerializable` are supported by `NetworkSerializer`, `RPC` s and `NetworkVariable` s. +Types implementing `INetworkSerializable` are supported by [`FastBufferReader` and `FastBufferWriter`](./fastbufferwriter-fastbufferreader.md), [`RPC`s'](../message-system/rpc.md), and [`NetworkVariable`s](../../basics/networkvariable.md). ```csharp - [Rpc(SendTo.Server)] -void MyServerRpc(MyComplexStruct myStruct) { /* ... */ } +void SpawnAtPointRpc(SpawnPoint spawnPoint) { /* ... */ } -void Update() +void DoSpawnHere() { - if (Input.GetKeyDown(KeyCode.P)) + var spawnPoint = new SpawnPoint { - MyServerRpc( - new MyComplexStruct - { - Position = transform.position, - Rotation = transform.rotation - }); // Client -> Server - } + Position = transform.position, + Rotation = transform.rotation + }; + SpawnAtPointRpc(spawnPoint); // Client -> Server } ``` @@ -59,7 +60,7 @@ The following example explores a more advanced use case. ```csharp -public struct MyMoveStruct : INetworkSerializable +public struct SpawnWithMovement : INetworkSerializable { public Vector3 Position; public Quaternion Rotation; @@ -125,7 +126,7 @@ Review the following example: ```csharp -public struct MyStructA : INetworkSerializable +public struct SpawnPoint : INetworkSerializable { public Vector3 Position; public Quaternion Rotation; @@ -137,17 +138,17 @@ public struct MyStructA : INetworkSerializable } } -public struct MyStructB : INetworkSerializable +public struct SpawnInfo : INetworkSerializable { public int SomeNumber; public string SomeText; - public MyStructA StructA; + public SpawnPoint SpawnPoint; void NetworkSerialize(BufferSerializer serializer) where T : IReaderWriter { serializer.SerializeValue(ref SomeNumber); serializer.SerializeValue(ref SomeText); - StructA.NetworkSerialize(serializer); + SpawnPoint.NetworkSerialize(serializer); } } ``` @@ -167,7 +168,7 @@ While you can have nested `INetworkSerializable` implementations (an `INetworkSe ```csharp /// This isn't supported. -public struct MyStructB : MyStructA +public struct SpawnInfo : SpawnPoint { public int SomeNumber; public string SomeText; @@ -232,4 +233,4 @@ Then declare this network variable like so: ```csharp NetworkVariable myVar = new NetworkVariable(); -``` \ No newline at end of file +``` diff --git a/com.unity.netcode.gameobjects/Documentation~/advanced-topics/serialization/inetworkserializebymemcpy.md b/com.unity.netcode.gameobjects/Documentation~/advanced-topics/serialization/inetworkserializebymemcpy.md index 7a684dbaba..212bfbf339 100644 --- a/com.unity.netcode.gameobjects/Documentation~/advanced-topics/serialization/inetworkserializebymemcpy.md +++ b/com.unity.netcode.gameobjects/Documentation~/advanced-topics/serialization/inetworkserializebymemcpy.md @@ -1,4 +1,7 @@ -# INetworkSerializeByMemcpy +# Serialize unmanaged structs with INetworkSerializeByMemcpy + +> [!NOTE] +> Read the [Serialization overview](./serialization/serialization-overview.md) page to understand the basics of serialization before using `INetworkSerializeByMemcpy` to serialize unmanaged structs. The `INetworkSerializeByMemcpy` interface is used to mark an unmanaged struct type as being trivially serializable over the network by directly copying the whole struct, byte-for-byte, as it appears in memory, into and out of the buffer. This can offer some benefits for performance compared to serializing one field at a time, especially if the struct has many fields in it, but it may be less efficient from a bandwidth-usage perspective, as fields will often be padded for memory alignment and you won't be able to "pack" any of the fields to optimize for space usage. @@ -14,7 +17,7 @@ public struct MyStruct : INetworkSerializeByMemcpy } ``` -If you have a type you wish to serialize that you know is compatible with this method of serialization, but don't have access to modify the struct to add this interface, you can wrap your values in `ForceNetworkSerializeByMemcpy` to enable it to be serialized this way. This works in both `RPC`s and `NetworkVariables`, as well as in other contexts such as `BufferSerializer<>`, `FastBufferReader`, and `FastBufferWriter`. +If you have a type you wish to serialize that you know is compatible with this method of serialization, but don't have access to modify the struct to add this interface, you can wrap your values in `ForceNetworkSerializeByMemcpy` to enable it to be serialized this way. This works in both [`RPC`s](../message-system/rpc.md) and [`NetworkVariable`s](../../basics/networkvariable.md), as well as in other contexts such as with [`BufferSerializer<>`](../bufferserializer.md) or [`FastBufferReader`, and `FastBufferWriter`](../fastbufferwriter-fastbufferreader.md). ```csharp public NetworkVariable> GuidVar; diff --git a/com.unity.netcode.gameobjects/Documentation~/advanced-topics/serialization/networkobject-serialization.md b/com.unity.netcode.gameobjects/Documentation~/advanced-topics/serialization/networkobject-serialization.md index 9ca778028d..466d545b4b 100644 --- a/com.unity.netcode.gameobjects/Documentation~/advanced-topics/serialization/networkobject-serialization.md +++ b/com.unity.netcode.gameobjects/Documentation~/advanced-topics/serialization/networkobject-serialization.md @@ -1,14 +1,18 @@ -# NetworkObject and NetworkBehaviour +# NetworkObject and NetworkBehaviour serialization -`GameObjects`, `NetworkObjects` and NetworkBehaviour aren't serializable types so they can't be used in `RPCs` or `NetworkVariables` by default. +> [!NOTE] +> Read the [Serialization overview](./serialization/serialization-overview.md) page to understand the basics of serialization before learning how to serialize `NetworkObjects` and `NetworkBehaviours`. + +`GameObject`, [`NetworkObject`](../../components/core/networkobject.md) and [`NetworkBehaviour`](../../components/core/networkbehaviour.md) aren't serializable types so they can't be used in [`RPC`s](../message-system/rpc.md) or [`NetworkVariable`s](../../basics/networkvariable.md) by default. -There are two convenience wrappers which can be used to send a reference to a NetworkObject or a NetworkBehaviour over RPCs or `NetworkVariables`. +There are two convenience wrappers which can be used to send a reference to a `NetworkObject` or a `NetworkBehaviour` over an `RPC` or in a `NetworkVariable`. ## NetworkObjectReference -`NetworkObjectReference` can be used to serialize a reference to a NetworkObject. It can only be used on already spawned `NetworkObjects`. +`NetworkObjectReference` can be used to serialize a reference to a `NetworkObject`. It can only be used on already spawned `NetworkObject`s. Here is an example of using NetworkObject reference to send a target NetworkObject over an RPC: + ```csharp public class Weapon : NetworkBehaviour { @@ -36,6 +40,7 @@ public class Weapon : NetworkBehaviour ### Implicit Operators There are also implicit operators which convert from/to `NetworkObject/GameObject` which can be used to simplify code. For instance the above example can also be written in the following way: + ```csharp public class Weapon : NetworkBehaviour { @@ -51,6 +56,7 @@ public class Weapon : NetworkBehaviour } } ``` + > [!NOTE] > The implicit conversion to NetworkObject / GameObject will result in `Null` if the reference can't be found. @@ -85,6 +91,6 @@ public class Weapon : NetworkBehaviour ## How NetworkObjectReference & NetworkBehaviourReference work -`NetworkObjectReference` and `NetworkBehaviourReference` are convenience wrappers which serialize the id of a NetworkObject when being sent and on the receiving end retrieve the corresponding ` ` with that id. `NetworkBehaviourReference` sends an additional index which is used to find the right NetworkBehaviour on the NetworkObject. +`NetworkObjectReference` and `NetworkBehaviourReference` are convenience wrappers which serialize the id of a `NetworkObject` when being sent and on the receiving end retrieve the corresponding `NetworkObject` with that id. `NetworkBehaviourReference` sends an additional index which is used to find the right `NetworkBehaviour` on the `NetworkObject`. -Both of them are structs implementing the `INetworkSerializable` interface. +Both of them are structs implementing the [`INetworkSerializable`](./inetworkserializable.md) interface. diff --git a/com.unity.netcode.gameobjects/Documentation~/advanced-topics/serialization/serialization-arrays.md b/com.unity.netcode.gameobjects/Documentation~/advanced-topics/serialization/serialization-arrays.md index c347f4c13e..5681e5952c 100644 --- a/com.unity.netcode.gameobjects/Documentation~/advanced-topics/serialization/serialization-arrays.md +++ b/com.unity.netcode.gameobjects/Documentation~/advanced-topics/serialization/serialization-arrays.md @@ -1,6 +1,9 @@ # Arrays and native containers -Netcode for GameObjects has built-in serialization code for arrays of [C# value-type primitives](cprimitives.md), like `int[]`, and [Unity primitive types](unity-primitives.md). Any arrays of types that aren't handled by the built-in serialization code, such as `string[]`, need to be handled using a container class or structure that implements the [`INetworkSerializable`](inetworkserializable.md) interface. +> [!NOTE] +> Read the [Serialization overview](./serialization/serialization-overview.md) page to understand the basics of serialization before learning how to serialize arrays and native containers. + +Netcode for GameObjects has built-in serialization code for arrays of [C# value-type primitives](cprimitives.md), like `int[]`, and [Unity primitive types](unity-primitives.md). Any arrays of types that aren't handled by the built-in serialization code, such as custom types, need to be handled using a container class or structure that implements the [`INetworkSerializable`](inetworkserializable.md) interface. ## Performance considerations @@ -17,37 +20,6 @@ Using built-in primitive types is fairly straightforward: void HelloServerRpc(int[] scores, Color[] colors) { /* ... */ } ``` -## INetworkSerializable implementation example - -There are many ways to handle sending an array of managed types. The following example is a simple `string` container class that implements `INetworkSerializable` and can be used as an array of "StringContainers": - -```csharp -[Rpc(SendTo.ClientsAndHost)] -void SendMessagesClientRpc(StringContainer[] messages) -{ - foreach (var stringContainer in stringContainers) - { - Debug.Log($"{stringContainer.SomeText}"); - } -} - -public class StringContainer : INetworkSerializable -{ - public string SomeText; - public void NetworkSerialize(BufferSerializer serializer) where T : IReaderWriter - { - if (serializer.IsWriter) - { - serializer.GetFastBufferWriter().WriteValueSafe(SomeText); - } - else - { - serializer.GetFastBufferReader().ReadValueSafe(out SomeText); - } - } -} -``` - ## Native containers Netcode for GameObjects supports `NativeArray` and `NativeList` native containers with built-in serialization, RPCs, and NetworkVariables. However, you can't nest either of these containers without causing a crash. @@ -77,3 +49,7 @@ To serialize a `NativeList` container, you must: > [!NOTE] > When using `NativeLists` within `INetworkSerializable`, the list `ref` value must be a valid, initialized `NativeList`. > NetworkVariables are similar that the value must be initialized before it can receive updates. For example, `public NetworkVariable> ByteListVar = new NetworkVariable>{Value = new NativeList(Allocator.Persistent)};`. RPCs do this automatically. + +## Generic collections + +For performance reasons, Netcode for GameObjects does not have built-in serialization code for C# generic collections. However, `NetworkVariable` does support [synchronizing generic collection types](../../basics/networkvariable.md#using-collections-with-networkvariables). diff --git a/com.unity.netcode.gameobjects/Documentation~/advanced-topics/serialization/serialization-overview.md b/com.unity.netcode.gameobjects/Documentation~/advanced-topics/serialization/serialization-overview.md new file mode 100644 index 0000000000..fb52e42773 --- /dev/null +++ b/com.unity.netcode.gameobjects/Documentation~/advanced-topics/serialization/serialization-overview.md @@ -0,0 +1,36 @@ +# Serialization overview + +Netcode for GameObjects uses a default serialization pipeline when using [`RPC`](../../rpc-landing.md)'s, [`NetworkVariable`](../../networkvariables-landing.md)s, or any other Netcode-related tasks that require serialization. The serialization pipeline looks like this: + +`` +Custom types => Built-in types => INetworkSerializable +`` + +When Netcode for GameObjects first receives a type, it checks for any custom types that you have registered for serialization, then it checks if it's a built-in type, such as a Vector3 or a float. These are handled by default. Otherwise, it checks if the type inherits [`INetworkSerializable`](inetworkserializable.md), and if it does, it calls its write methods. + +By default, any type that satisfies the unmanaged generic constraint can be automatically serialized as RPC parameters. This includes all basic types (bool, byte, int, float, enum, for example), as well as any structs that contain only these basic types. + +Serialization and deserialization is done via the structs [`FastBufferWriter` and `FastBufferReader`](fastbufferwriter-fastbufferreader.md). These have methods for serializing individual types and methods for serializing packed numbers, but in particular provide a high-performance method called `WriteValue()/ReadValue()` (for Writers and Readers, respectively) that can extremely quickly write an entire unmanaged struct to a buffer. + +`FastBufferWriter` and `FastBufferReader` also contain the functions `FastBufferWriter.WriteNetworkSerializable()` and `FastBufferReader.ReadNetworkSerializable` for writing and reading values that use the `INetworkSerializable` interface. + +## Built-in serialization + +* [C# primitives](./cprimitives.md) +* [Unity primitives](./unity-primitives.md) +* [Enum types](./enum-types.md) +* [Arrays](./serialization-arrays.md) +* [Collections](../../basics/networkvariable.md#using-collections-with-networkvariables) + +## Custom serialization + +* [INetworkSerializable](./inetworkserializable.md) +* [INetworkSerializeByMemcpy](./inetworkserializebymemcpy.md) +* [Customizing serialization](../custom-serialization.md) +* [Custom NetworkVariable implementations](../../basics/custom-networkvariables.md) + +## Additional resources + +* [NetworkObject serialization](./networkobject-serialization.md) +* [FastBufferWriter and FastBufferReader](../fastbufferwriter-fastbufferreader.md) +* [BufferSerializer](../bufferserializer.md) diff --git a/com.unity.netcode.gameobjects/Documentation~/basics/custom-networkvariables.md b/com.unity.netcode.gameobjects/Documentation~/basics/custom-networkvariables.md index c2c1f97286..de160d26d2 100644 --- a/com.unity.netcode.gameobjects/Documentation~/basics/custom-networkvariables.md +++ b/com.unity.netcode.gameobjects/Documentation~/basics/custom-networkvariables.md @@ -2,6 +2,9 @@ In addition to the standard [`NetworkVariable`s](networkvariable.md) available in Netcode for GameObjects, you can also create custom `NetworkVariable`s for advanced implementations. The `NetworkVariable` and `NetworkList` classes were created as `NetworkVariableBase` class implementation examples. While the `NetworkVariable` class is considered production ready, you might run into scenarios where you have a more advanced implementation in mind. In this case, you can create your own custom implementation. +> [!NOTE] +> Read the [Serialization overview](./serialization/serialization-overview.md) page to understand how Netcode for GameObjects handles serialization. + To create your own `NetworkVariableBase`-derived container, you should: 1. Create a class deriving from `NetworkVariableBase`. @@ -16,11 +19,14 @@ To create your own `NetworkVariableBase`-derived container, you should: The way you read and write `NetworkVariable`s changes depending on the type you use. -* Known, non-generic types: Use `FastBufferReader.ReadValue` to read from and `FastBufferWriter.WriteValue` to write to the `NetworkVariable` value. -* Integer types: This type gives you the option to use `BytePacker` and `ByteUnpacker` to compress the `NetworkVariable` value. This process can save bandwidth but adds CPU processing time. +* Known, non-generic types can use [`FastBufferWriter` and `FastBufferReader`](../advanced-topics/fastbufferwriter-fastbufferreader.md) to serialize the `NetworkVariable` value. +* Integer types: This type gives you the option to use [`BytePacker` and `ByteUnpacker`](../advanced-topics/fastbufferwriter-fastbufferreader.md#packing) to compress the `NetworkVariable` value. This process can save bandwidth but adds CPU processing time. * Generic types: Use serializers that Unity generates based on types discovered during a compile-time code generation process. This means you need to tell Unity's code generation algorithm which types to generate serializers for. To tell Unity which types to serialize, use the following methods: * Use `GenerateSerializationForTypeAttribute` to serialize hard-coded types. * Use `GenerateSerializationForGenericParameterAttribute` to serialize generic types. +* Types implementing [`INetworkSerializable`](../advanced-topics/serialization/inetworkserializable.md) can also be serialized using `FastBufferWriter` and `FastBufferReader`. + +Read more about [custom serialization](../advanced-topics/custom-serialization.md) for more approaches to customizing serialization. ### Serialize a hard-coded type @@ -66,175 +72,22 @@ For dynamically-allocated types with a value that isn't `null` (for example, man You can use `AreEqual` to determine if a value is different from the value that `Duplicate` cached. This avoids sending the same value multiple times. You can also use the previous value that `Duplicate` cached to calculate deltas to use in `ReadDelta` and `WriteDelta`. -The type you use must be serializable according to the [support types list above](#supported-types). Each type needs its own serializer instantiated, so this step tells the codegen which types to create serializers for. Unity's code generator assumes that all `NetworkVariable` types exist as fields inside NetworkBehaviour types. This means that Unity only inspects fields inside NetworkBehaviour types to identify the types to create serializers for. +The type you use must be serializable according to the [supported types list](./networkvariable.md#supported-types). Each type needs its own serializer instantiated, so this step tells the codegen which types to create serializers for. Unity's code generator assumes that all `NetworkVariable` types exist as fields inside NetworkBehaviour types. This means that Unity only inspects fields inside NetworkBehaviour types to identify the types to create serializers for. -## Custom NetworkVariable example +> [!NOTE] +> These attributes won't generate delta serialization. If you would like to customize how deltas are sent when a custom value has partially changed, refer to [`UserNetworkVariableSerialization`](../advanced-topics/custom-serialization.md#networkvariable). + +## Custom NetworkVariableBase example This example shows a custom `NetworkVariable` type to help you understand how you might implement such a type. In the current version of Netcode for GameObjects, this example is possible without using a custom `NetworkVariable` type; however, for more complex situations that aren't natively supported, this basic example should help inform you of how to approach the implementation: - ```csharp - /// Using MyCustomNetworkVariable within a NetworkBehaviour - public class TestMyCustomNetworkVariable : NetworkBehaviour - { - public MyCustomNetworkVariable CustomNetworkVariable = new MyCustomNetworkVariable(); - public MyCustomGenericNetworkVariable CustomGenericNetworkVariable = new MyCustomGenericNetworkVariable(); - public override void OnNetworkSpawn() - { - if (IsServer) - { - for (int i = 0; i < 4; i++) - { - var someData = new SomeData(); - someData.SomeFloatData = (float)i; - someData.SomeIntData = i; - someData.SomeListOfValues.Add((ulong)i + 1000000); - someData.SomeListOfValues.Add((ulong)i + 2000000); - someData.SomeListOfValues.Add((ulong)i + 3000000); - CustomNetworkVariable.SomeDataToSynchronize.Add(someData); - CustomNetworkVariable.SetDirty(true); - - CustomGenericNetworkVariable.SomeDataToSynchronize.Add(i); - CustomGenericNetworkVariable.SetDirty(true); - } - } - } - } - - /// Bare minimum example of NetworkVariableBase derived class - [Serializable] - public class MyCustomNetworkVariable : NetworkVariableBase - { - /// Managed list of class instances - public List SomeDataToSynchronize = new List(); - - /// - /// Writes the complete state of the variable to the writer - /// - /// The stream to write the state to - public override void WriteField(FastBufferWriter writer) - { - // Serialize the data we need to synchronize - writer.WriteValueSafe(SomeDataToSynchronize.Count); - foreach (var dataEntry in SomeDataToSynchronize) - { - writer.WriteValueSafe(dataEntry.SomeIntData); - writer.WriteValueSafe(dataEntry.SomeFloatData); - writer.WriteValueSafe(dataEntry.SomeListOfValues.Count); - foreach (var valueItem in dataEntry.SomeListOfValues) - { - writer.WriteValueSafe(valueItem); - } - } - } - - /// - /// Reads the complete state from the reader and applies it - /// - /// The stream to read the state from - public override void ReadField(FastBufferReader reader) - { - // De-Serialize the data being synchronized - var itemsToUpdate = (int)0; - reader.ReadValueSafe(out itemsToUpdate); - SomeDataToSynchronize.Clear(); - for (int i = 0; i < itemsToUpdate; i++) - { - var newEntry = new SomeData(); - reader.ReadValueSafe(out newEntry.SomeIntData); - reader.ReadValueSafe(out newEntry.SomeFloatData); - var itemsCount = (int)0; - var tempValue = (ulong)0; - reader.ReadValueSafe(out itemsCount); - newEntry.SomeListOfValues.Clear(); - for (int j = 0; j < itemsCount; j++) - { - reader.ReadValueSafe(out tempValue); - newEntry.SomeListOfValues.Add(tempValue); - } - SomeDataToSynchronize.Add(newEntry); - } - } - - public override void ReadDelta(FastBufferReader reader, bool keepDirtyDelta) - { - // Do nothing for this example - } - - public override void WriteDelta(FastBufferWriter writer) - { - // Do nothing for this example - } - } - - /// Bare minimum example of generic NetworkVariableBase derived class - [Serializable] - [GenerateSerializationForGenericParameter(0)] - public class MyCustomGenericNetworkVariable : NetworkVariableBase - { - /// Managed list of class instances - public List SomeDataToSynchronize = new List(); - - /// - /// Writes the complete state of the variable to the writer - /// - /// The stream to write the state to - public override void WriteField(FastBufferWriter writer) - { - // Serialize the data we need to synchronize - writer.WriteValueSafe(SomeDataToSynchronize.Count); - for (var i = 0; i < SomeDataToSynchronize.Count; ++i) - { - var dataEntry = SomeDataToSynchronize[i]; - // NetworkVariableSerialization is used for serializing generic types - NetworkVariableSerialization.Write(writer, ref dataEntry); - } - } - - /// - /// Reads the complete state from the reader and applies it - /// - /// The stream to read the state from - public override void ReadField(FastBufferReader reader) - { - // De-Serialize the data being synchronized - var itemsToUpdate = (int)0; - reader.ReadValueSafe(out itemsToUpdate); - SomeDataToSynchronize.Clear(); - for (int i = 0; i < itemsToUpdate; i++) - { - T newEntry = default; - // NetworkVariableSerialization is used for serializing generic types - NetworkVariableSerialization.Read(reader, ref newEntry); - SomeDataToSynchronize.Add(newEntry); - } - } - - public override void ReadDelta(FastBufferReader reader, bool keepDirtyDelta) - { - // Do nothing for this example - } - - public override void WriteDelta(FastBufferWriter writer) - { - // Do nothing for this example - } - } - - /// Example managed class used as the item type in the - /// MyCustomNetworkVariable.SomeDataToSynchronize list - [Serializable] - public class SomeData - { - public int SomeIntData = default; - public float SomeFloatData = default; - public List SomeListOfValues = new List(); - } - ``` +[!code-cs[](../../Tests/Runtime/DocumentationCodeSamples/NetworkVariable/CustomNetworkVariable.cs#TestMyCustomNetworkVariable)] While the above example isn't the recommended way to synchronize a list where the number or order of elements in the list often changes, it's an example of how you can define your own rules using `NetworkVariableBase`. You can test the code above by: -- Using the above code with a project that includes Netcode for GameObjects v1.0 (or higher). + +- Using the above code with a project that includes Netcode for GameObjects. - Adding the `TestMyCustomNetworkVariable` component to an in-scene placed NetworkObject. - Creating a stand alone build and running that as a host or server. - Running the same scene within the Editor and connecting as a client. diff --git a/com.unity.netcode.gameobjects/Documentation~/basics/networkvariable.md b/com.unity.netcode.gameobjects/Documentation~/basics/networkvariable.md index df9b3bb506..cfdd037143 100644 --- a/com.unity.netcode.gameobjects/Documentation~/basics/networkvariable.md +++ b/com.unity.netcode.gameobjects/Documentation~/basics/networkvariable.md @@ -552,7 +552,7 @@ public class PlayerState : NetworkBehaviour ## Complex types -Almost all of the examples on this page have been focused around numeric [value types](https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/builtin-types/value-types). Netcode for GameObjects also supports complex types and can support both unmanaged types *and* managed types (although avoiding managed types where possible will improve your game's performance). +Any type that implements the [`INetworkSerializable`](../advanced-topics/serialization/inetworkserializable.md) interface can be used inside a `NetworkVariable`. To synchronize a custom type that is not supported by the default serialization provided by Netcode for GameObjects, implement this interface to allow it to be used inside a `NetworkVariable`. ### Synchronizing complex types example @@ -582,7 +582,7 @@ public class PlayerState : NetworkBehaviour void Awake() { //NetworkList can't be initialized at declaration time like NetworkVariable. It must be initialized in Awake instead. - //If you do initialize at declaration, you will run into Memmory leak errors. + //If you do initialize at declaration, you will run into Memory leak errors. TeamAreaWeaponBoosters = new NetworkList(); } @@ -707,88 +707,12 @@ public struct AreaWeaponBooster : INetworkSerializable, System.IEquatable [!NOTE] -> The `NetworkVariable` and `NetworkList` classes were created as `NetworkVariableBase` class implementation examples. While the `NetworkVariable` class is considered production ready, you might run into scenarios where you have a more advanced implementation in mind. In this case, we encourage you to create your own custom implementation. - -To create your own `NetworkVariableBase` derived container, you should: - -- Create a class deriving from `NetworkVariableBase`. - -- Assure the the following methods are overridden: - - `void WriteField(FastBufferWriter writer)` - - `void ReadField(FastBufferReader reader)` - - `void WriteDelta(FastBufferWriter writer)` - - `void ReadDelta(FastBufferReader reader, bool keepDirtyDelta)` -- Depdending upon your custom `NetworkVariableBase` container, you might look at `NetworkVariable` or `NetworkList` to see how those two examples were implemented. - - - -#### NetworkVariableSerialization<T> - -The way you read and write network variables changes depending on the type you use. - -* Known, non-generic types: Use `FastBufferReader.ReadValue` to read from and `FastBufferWriter.WriteValue` to write to the network variable value. -* Integer types: This type gives you the option to use `BytePacker` and `ByteUnpacker` to compress the network variable value. This process can save bandwidth but adds CPU processing time. -* Generic types: Use serializers that Unity generates based on types discovered during a compile-time code generation process. This means you need to tell Unity's code generation algorithm which types to generate serializers for. To tell Unity which types to serialize, use the following methods: - * Use `GenerateSerializationForTypeAttribute` to serialize hard-coded types. - * Use `GenerateSerializationForGenericParameterAttribute` to serialize generic types. - To learn how to use these methods, refer to [Network variable serialization](#network-variable-serialization). - -##### Tell Unity to serialize a hard-coded type -The following code example uses `GenerateSerializationForTypeAttribute` to generate serialization for a specific hard-coded type: -```csharp -[GenerateSerializationForType(typeof(Foo))] -public class MyNetworkVariableTypeUsingFoo : NetworkVariableBase {} -``` - -You can call a type that you know the name of with the `FastBufferReader` or `FastBufferWriter` methods. These methods don't work for Generic types because the name of the type is unknown. -##### Tell Unity to serialize a generic type -The following code example uses `GenerateSerializationForGenericParameterAttribute` to generate serialization for a specific Generic parameter in your `NetworkVariable` type: -```csharp -[GenerateSerializationForGenericParameter(0)] -public class MyNetworkVariableType : NetworkVariableBase {} -``` - -This attribute accepts an integer that indicates which parameter in the type to generate serialization for. This value is 0-indexed, which means that the first type is 0, the second type is 1, and so on. -The following code example places the attribute more than once on one class to generate serialization for multiple types, in this case,`TFirstType` and `TSecondType: - -```csharp -[GenerateSerializationForGenericParameter(0)] -[GenerateSerializationForGenericParameter(1)] -public class MyNetworkVariableType : NetworkVariableBase {} -``` - - -The `GenerateSerializationForGenericParameterAttribute` and `GenerateSerializationForTypeAttribute` attributes make Unity's code generation create the following methods: - -```csharp -NetworkVariableSerialization.Write(FastBufferWriter writer, ref T value); -NetworkVariableSerialization.Read(FastBufferWriter writer, ref T value); -NetworkVariableSerialization.Duplicate(in T value, ref T duplicatedValue); -NetworkVariableSerialization.AreEqual(in T a, in T b); -``` - -For dynamically allocated types with a value that isn't `null` (for example, managed types and collections like NativeArray and NativeList) call `Read` to read the value in the existing object and write data into it directy (in-place). This avoids more allocations. - -You can use `AreEqual` to determine if a value is different from the value that `Duplicate` cached. This avoids sending the same value multiple times. You can also use the previous value that `Duplicate` cached to calculate deltas to use in `ReadDelta` and `WriteDelta`. - -The type you use must be serializable according to the "Supported Types" list above. Each type needs its own serializer instantiated, so this step tells the codegen which types to create serializers for. - -> [!NOTE] Unity's code generator assumes that all `NetworkVariable` types exist as fields inside NetworkBehaviour types. This means that Unity only inspects fields inside NetworkBehaviour types to identify the types to create serializers for. - - ### Custom NetworkVariable Example - -This example shows a custom `NetworkVariable` type to help you understand how you might implement such a type. In the current version of Netcode for GameObjects, this example is possible without using a custom `NetworkVariable` type; however, for more complex situations that aren't natively supported, this basic example should help inform you of how to approach the implementation: - -Looking at the read and write segments of code within `AreaWeaponBooster.NetworkSerialize`, the nested complex type property `ApplyWeaponBooster` handles its own serialization and de-serialization. The `ApplyWeaponBooster`'s implemented `NetworkSerialize` method serializes and deserializes any `AreaWeaponBooster` type property. This design approach can help reduce code replication while providing a more modular foundation to build even more complex, nested types. - +Further information on customizing the serialization of complex types can be found in [custom serialization](../advanced-topics/custom-serialization.md#networkvariable). ## Strings -While `NetworkVariable` does support managed `INetworkSerializable` types, strings aren't in the list of supported types. This is because strings in C# are immutable types, preventing them from being deserialized in-place, so every update to a `NetworkVariable` would cause a Garbage Collected allocation to create the new string, which may lead to performance problems. +While `NetworkVariable` does support managed `INetworkSerializable` types, strings aren't in the [list of supported types](#supported-types). This is because strings in C# are immutable types, preventing them from being deserialized in-place, so every update to a `NetworkVariable` would cause a Garbage Collected allocation to create the new string, which may lead to performance problems. -While it's technically possible to support strings using custom serialization through `UserNetworkVariableSerialization`, it isn't recommended to do so due to the performance implications that come with it. Instead, we recommend using one of the `Unity.Collections.FixedString` value types. In the below example, we used a `FixedString128Bytes` as the `NetworkVariable` value type. On the server side, it changes the string value each time you press the space bar on the server or host instance. Joining clients will be synchronized with the current value applied on the server side, and each time you hit the space bar on the server side, the client synchronizes with the changed string. +While it's technically possible to support strings using custom serialization through [`UserNetworkVariableSerialization`](../advanced-topics/custom-serialization.md#networkvariable), it isn't recommended to do so due to the performance implications that come with it. Instead, it's recommended to use one of the `Unity.Collections.FixedString` value types. In the below example, a `FixedString128Bytes` is the `NetworkVariable` value type. On the server side, it changes the string value each time you press the space bar on the server or host instance. Joining clients will be synchronized with the current value applied on the server side, and each time you hit the space bar on the server side, the client synchronizes with the changed string. > [!NOTE] > `NetworkVariable` won't serialize the entire 128 bytes each time the `Value` is changed. Only the number of bytes that are actually used to store the string value will be sent, no matter which size of `FixedString` you use. @@ -855,3 +779,9 @@ public class TestFixedString : NetworkBehaviour > [!NOTE] > The above example uses a pre-set list of strings to cycle through for example purposes only. If you have a predefined set of text strings as part of your actual design then you would not want to use a FixedString to handle synchronizing the changes to `m_TextString`. Instead, you would want to use a `uint` for the type `T` where the `uint` was the index of the string message to apply to `m_TextString`. + +### Delta updates + +To save bandwidth, `NetworkVariables` can send delta updates. A delta is a compact description of what changed since the last sync. By default, [collection types](#using-collections-with-networkvariables) all support sending delta updates. This means adding a single item to a large list doesn't need to send the entire list over the network. For complex types, it is often worth considering whether sending deltas will improve bandwidth. Delta serialization can be configured via [`UserNetworkVariableSerialization`](../advanced-topics/custom-serialization.md#serializing-delta-updates). + +When a NetworkObject is spawned or a late-joining client first sees it, every `NetworkVariable` will be serialized in full. This allows game clients to start receiving deltas from a known state. diff --git a/com.unity.netcode.gameobjects/Documentation~/serialization.md b/com.unity.netcode.gameobjects/Documentation~/serialization.md index fb39fad334..4dfd16f484 100644 --- a/com.unity.netcode.gameobjects/Documentation~/serialization.md +++ b/com.unity.netcode.gameobjects/Documentation~/serialization.md @@ -4,11 +4,14 @@ Netcode for GameObjects has built-in serialization support for C# and Unity prim | **Topic** | **Description** | | :------------------------------ | :------------------------------- | +| **[Serialization overview](advanced-topics/serialization/serialization-overview.md)** | An overview of how Netcode for GameObjects handles serialization. | | **[C# primitives](advanced-topics/serialization/cprimitives.md)** | C# primitive types are serialized by built-in serialization code. These types include `bool`, `char`, `sbyte`, `byte`, `short`, `ushort`, `int`, `uint`, `long`, `ulong`, `float`, `double`, and `string`. | | **[Unity primitives](advanced-topics/serialization/unity-primitives.md)** | Unity Primitive `Color`, `Color32`, `Vector2`, `Vector3`, `Vector4`, `Quaternion`, `Ray`, `Ray2D` types will be serialized by built-in serialization code. | | **[Enum types](advanced-topics/serialization/enum-types.md)** | A user-defined enum type will be serialized by built-in serialization code (with underlying integer type). | | **[Arrays](advanced-topics/serialization/serialization-arrays.md)** | Netcode for GameObjects has built-in serialization code for arrays of [C# value-type primitives](advanced-topics/serialization/cprimitives.md), like `int[]`, and [Unity primitive types](advanced-topics/serialization/unity-primitives.md). Any arrays of types that aren't handled by the built-in serialization code, such as `string[]`, need to be handled using a container class or structure that implements the [`INetworkSerializable`](advanced-topics/serialization/inetworkserializable.md) interface. | -| **[INetworkSerializable](advanced-topics/serialization/inetworkserializable.md)** | You can use the `INetworkSerializable` interface to define custom serializable types. | +| **[Customize serializable types with INetworkSerializable](advanced-topics/serialization/inetworkserializable.md)** | You can use the `INetworkSerializable` interface to define custom serializable types. | +| **[Serialize unmanaged structs with INetworkSerializeByMemcpy](advanced-topics/serialization/inetworkserializebymemcpy.md)** | You can use the `INetworkSerializeByMemcpy` interface to mark an unmanaged struct type as being trivially serializable. | | **[Custom serialization](advanced-topics/custom-serialization.md)** | Create custom serialization types. | | **[NetworkObject serialization](advanced-topics/serialization/networkobject-serialization.md)** | `GameObjects`, `NetworkObjects` and NetworkBehaviour aren't serializable types so they can't be used in `RPCs` or `NetworkVariables` by default. There are two convenience wrappers which can be used to send a reference to a NetworkObject or a NetworkBehaviour over RPCs or `NetworkVariables`. | -| **[FastBufferWriter and FastBufferReader](advanced-topics/fastbufferwriter-fastbufferreader.md)** | The serialization and deserialization is done via `FastBufferWriter` and `FastBufferReader`. These have methods for serializing individual types and methods for serializing packed numbers, but in particular provide a high-performance method called `WriteValue()/ReadValue()` (for Writers and Readers, respectively) that can extremely quickly write an entire unmanaged struct to a buffer. | \ No newline at end of file +| **[FastBufferWriter and FastBufferReader](advanced-topics/fastbufferwriter-fastbufferreader.md)** | The serialization and deserialization is done via `FastBufferWriter` and `FastBufferReader`. These have methods for serializing individual types and methods for serializing packed numbers, but in particular provide a high-performance method called `WriteValue()/ReadValue()` (for Writers and Readers, respectively) that can extremely quickly write an entire unmanaged struct to a buffer. | +| **[BufferSerializer](advanced-topics/bufferserializer.md)** | A bi-directional serializer primarily used for serializing within [`INetworkSerializable`](serialization/inetworkserializable.md) types. | diff --git a/com.unity.netcode.gameobjects/Runtime/NetworkVariable/Serialization/TypedSerializerImplementations.cs b/com.unity.netcode.gameobjects/Runtime/NetworkVariable/Serialization/TypedSerializerImplementations.cs index e1242378dd..00dd2978b6 100644 --- a/com.unity.netcode.gameobjects/Runtime/NetworkVariable/Serialization/TypedSerializerImplementations.cs +++ b/com.unity.netcode.gameobjects/Runtime/NetworkVariable/Serialization/TypedSerializerImplementations.cs @@ -243,11 +243,23 @@ public void Read(FastBufferReader reader, ref T value) public void WriteDelta(FastBufferWriter writer, ref T value, ref T previousValue) { + if (UserNetworkVariableSerialization.WriteDelta != null && UserNetworkVariableSerialization.ReadDelta != null) + { + UserNetworkVariableSerialization.WriteDelta(writer, value, previousValue); + return; + } + Write(writer, ref value); } public void ReadDelta(FastBufferReader reader, ref T value) { + if (UserNetworkVariableSerialization.WriteDelta != null && UserNetworkVariableSerialization.ReadDelta != null) + { + UserNetworkVariableSerialization.ReadDelta(reader, ref value); + return; + } + Read(reader, ref value); } @@ -258,6 +270,12 @@ void INetworkVariableSerializer.ReadWithAllocator(FastBufferReader reader, ou public void Duplicate(in T value, ref T duplicatedValue) { + if (UserNetworkVariableSerialization.DuplicateValue != null) + { + UserNetworkVariableSerialization.DuplicateValue(value, ref duplicatedValue); + return; + } + duplicatedValue = value; } } @@ -959,6 +977,12 @@ void INetworkVariableSerializer.ReadWithAllocator(FastBufferReader reader, ou public void Duplicate(in T value, ref T duplicatedValue) { + if (UserNetworkVariableSerialization.DuplicateValue != null) + { + UserNetworkVariableSerialization.DuplicateValue(value, ref duplicatedValue); + return; + } + duplicatedValue = value; } } @@ -1128,6 +1152,12 @@ void INetworkVariableSerializer.ReadWithAllocator(FastBufferReader reader, ou public void Duplicate(in T value, ref T duplicatedValue) { + if (UserNetworkVariableSerialization.DuplicateValue != null) + { + UserNetworkVariableSerialization.DuplicateValue(value, ref duplicatedValue); + return; + } + using var writer = new FastBufferWriter(256, Allocator.Temp, int.MaxValue); var refValue = value; Write(writer, ref refValue); diff --git a/com.unity.netcode.gameobjects/Tests/Editor/DocumentationCodeSamples.meta b/com.unity.netcode.gameobjects/Tests/Editor/DocumentationCodeSamples.meta new file mode 100644 index 0000000000..bc0cce73b4 --- /dev/null +++ b/com.unity.netcode.gameobjects/Tests/Editor/DocumentationCodeSamples.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 0f959eedfa8244038f30beb1f00b8cf0 +timeCreated: 1780100790 \ No newline at end of file diff --git a/com.unity.netcode.gameobjects/Tests/Editor/DocumentationCodeSamples/README.md b/com.unity.netcode.gameobjects/Tests/Editor/DocumentationCodeSamples/README.md new file mode 100644 index 0000000000..33ca58ffcc --- /dev/null +++ b/com.unity.netcode.gameobjects/Tests/Editor/DocumentationCodeSamples/README.md @@ -0,0 +1,31 @@ +# Code documentation + +Use this folder for any code snippets that only need to compile. + +Any code snippets that you want to run tests on should be put in [Runtime tests](../../Runtime/Documentation/README.md). + +To embed code in documentation, use the following tag + +```md +[!code-cs[](../../Tests/Editor/DocumentationCodeSamples/.cs#SomeRegionName)] +``` + +With the code formatted like this + +```cs +namespace DocumentationCodeSamples +{ + internal MyTestClass + { + #region SomeRegionName + // All the code in this region block will be embedded without indentation in the docs. + #endregion + + [Test] + public void TestOfDocumentationCode() + { + ... + } + } +} +``` diff --git a/com.unity.netcode.gameobjects/Tests/Editor/DocumentationCodeSamples/README.md.meta b/com.unity.netcode.gameobjects/Tests/Editor/DocumentationCodeSamples/README.md.meta new file mode 100644 index 0000000000..d43f352993 --- /dev/null +++ b/com.unity.netcode.gameobjects/Tests/Editor/DocumentationCodeSamples/README.md.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: a6afff0520856465a9f94b9a5bcebf24 +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/com.unity.netcode.gameobjects/Tests/Editor/DocumentationCodeSamples/Serialization.meta b/com.unity.netcode.gameobjects/Tests/Editor/DocumentationCodeSamples/Serialization.meta new file mode 100644 index 0000000000..677d24fb22 --- /dev/null +++ b/com.unity.netcode.gameobjects/Tests/Editor/DocumentationCodeSamples/Serialization.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 9371809eac694274ad78a6e7faf27e69 +timeCreated: 1780674489 \ No newline at end of file diff --git a/com.unity.netcode.gameobjects/Tests/Editor/DocumentationCodeSamples/Serialization/SerializationCustomization.cs b/com.unity.netcode.gameobjects/Tests/Editor/DocumentationCodeSamples/Serialization/SerializationCustomization.cs new file mode 100644 index 0000000000..3f70a15cb7 --- /dev/null +++ b/com.unity.netcode.gameobjects/Tests/Editor/DocumentationCodeSamples/Serialization/SerializationCustomization.cs @@ -0,0 +1,114 @@ +using NUnit.Framework; +using Unity.Collections; +using Unity.Netcode; + +namespace DocumentationCodeSamples +{ + #region HealthStruct + /// Container for storing health data for a player or item. + public struct Health + { + /// + /// The maximum health that this player or item can have. + /// This is unlikely to change often. + /// + public uint MaxHealth; + + /// + /// The current level of health that this player or item has. + /// This is likely to change regularly. + /// + public int CurrentHealth; + } + #endregion + + #region FastBuffer + /// Tells the Netcode how to serialize and deserialize our custom type. + // The class name doesn't matter here. + public static class FastBufferExtensions + { + /// + /// Extension method to override the serialization for a custom type. + /// + /// Buffer to write values into. + /// The type to customize or override. + public static void WriteValueSafe(this FastBufferWriter writer, in Health health) + { + writer.WriteValueSafe(health.MaxHealth); + writer.WriteValueSafe(health.CurrentHealth); + } + + /// + /// Extension method to override the de-serialization for a custom type. + /// + /// Buffer to read values from. + /// The type to customize or override. + public static void ReadValueSafe(this FastBufferReader reader, out Health health) + { + reader.ReadValueSafe(out uint max); + reader.ReadValueSafe(out int current); + health = new Health { MaxHealth = max, CurrentHealth = current }; + } + } + #endregion + + #region BufferSerializer + /// Tells the how to serialize and deserialize our custom type. + // The class name doesn't matter here. + public static class BufferSerializerExtensions + { + /// + /// Extension method to override bi-directional serialization for a custom type. + /// + /// Bi-directional serial + /// The type to customize or override. + /// Boilerplate syntax to enable the bi-directional serialization. + public static void SerializeValue(this BufferSerializer serializer, ref Health health) where TReaderWriter : IReaderWriter + { + // Because the BufferSerializer already knows how to read and write the primitive types + // We can use the existing BufferSerializer serialization. + serializer.SerializeValue(ref health.MaxHealth); + serializer.SerializeValue(ref health.CurrentHealth); + } + } + #endregion + + internal class TestSerializationDocs + { + [Test] + public void TestFastBufferSerialization() + { + var healthToTest = new Health { MaxHealth = 123, CurrentHealth = 89 }; + var expected = healthToTest; + + using var writer = new FastBufferWriter(256, Allocator.Temp, int.MaxValue); + writer.WriteValueSafe(healthToTest); + + using var reader = new FastBufferReader(writer, Allocator.None); + reader.ReadValueSafe(out Health readHealth); + + Assert.AreEqual(expected.MaxHealth, readHealth.MaxHealth); + Assert.AreEqual(expected.CurrentHealth, readHealth.CurrentHealth); + } + + [Test] + public void TestBufferSerializerSerialization() + { + var healthToTest = new Health { MaxHealth = 456, CurrentHealth = 78 }; + var expected = healthToTest; + + using var writer = new FastBufferWriter(256, Allocator.Temp, int.MaxValue); + var bufferWriter = new BufferSerializer(new BufferSerializerWriter(writer)); + bufferWriter.SerializeValue(ref healthToTest); + + using var tempReader = new FastBufferReader(bufferWriter.GetFastBufferWriter(), Allocator.None); + var bufferReader = new BufferSerializer(new BufferSerializerReader(tempReader)); + var readHealth = new Health(); + bufferReader.SerializeValue(ref readHealth); + + Assert.AreEqual(expected.MaxHealth, readHealth.MaxHealth); + Assert.AreEqual(expected.CurrentHealth, readHealth.CurrentHealth); + + } + } +} diff --git a/com.unity.netcode.gameobjects/Tests/Editor/DocumentationCodeSamples/Serialization/SerializationCustomization.cs.meta b/com.unity.netcode.gameobjects/Tests/Editor/DocumentationCodeSamples/Serialization/SerializationCustomization.cs.meta new file mode 100644 index 0000000000..a39141e43d --- /dev/null +++ b/com.unity.netcode.gameobjects/Tests/Editor/DocumentationCodeSamples/Serialization/SerializationCustomization.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 08949d7abc1249418192e44999279f89 +timeCreated: 1780101010 \ No newline at end of file diff --git a/com.unity.netcode.gameobjects/Tests/Runtime/DocumentationCodeSamples.meta b/com.unity.netcode.gameobjects/Tests/Runtime/DocumentationCodeSamples.meta new file mode 100644 index 0000000000..aea9d686c3 --- /dev/null +++ b/com.unity.netcode.gameobjects/Tests/Runtime/DocumentationCodeSamples.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: e017ce40eb7744e03bc0a83fa18ddcc3 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/com.unity.netcode.gameobjects/Tests/Runtime/DocumentationCodeSamples/NetworkVariable.meta b/com.unity.netcode.gameobjects/Tests/Runtime/DocumentationCodeSamples/NetworkVariable.meta new file mode 100644 index 0000000000..6fc7d1074d --- /dev/null +++ b/com.unity.netcode.gameobjects/Tests/Runtime/DocumentationCodeSamples/NetworkVariable.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 446ef307887e3446f896c899f43af442 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/com.unity.netcode.gameobjects/Tests/Runtime/DocumentationCodeSamples/NetworkVariable/CustomNetworkVariable.cs b/com.unity.netcode.gameobjects/Tests/Runtime/DocumentationCodeSamples/NetworkVariable/CustomNetworkVariable.cs new file mode 100644 index 0000000000..74fb7fe157 --- /dev/null +++ b/com.unity.netcode.gameobjects/Tests/Runtime/DocumentationCodeSamples/NetworkVariable/CustomNetworkVariable.cs @@ -0,0 +1,239 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using NUnit.Framework; +using Unity.Netcode; +using Unity.Netcode.TestHelpers.Runtime; +using UnityEngine; +using UnityEngine.TestTools; + +namespace DocumentationCodeSamples +{ + #region TestMyCustomNetworkVariable + // Using MyCustomNetworkVariable within a NetworkBehaviour + internal class TestMyCustomNetworkVariable : NetworkBehaviour + { + public MyCustomNetworkVariable CustomNetworkVariable = new MyCustomNetworkVariable(); + public MyCustomGenericNetworkVariable CustomGenericNetworkVariable = new MyCustomGenericNetworkVariable(); + + public override void OnNetworkSpawn() + { + if (IsServer) + { + for (int i = 0; i < 4; i++) + { + var someData = new SomeData + { + SomeFloatData = i, + SomeIntData = i + }; + someData.SomeListOfValues.Add((ulong)i + 1000000); + someData.SomeListOfValues.Add((ulong)i + 2000000); + someData.SomeListOfValues.Add((ulong)i + 3000000); + CustomNetworkVariable.SomeDataToSynchronize.Add(someData); + CustomNetworkVariable.SetDirty(true); + + CustomGenericNetworkVariable.SomeDataToSynchronize.Add(i); + CustomGenericNetworkVariable.SetDirty(true); + } + } + } + } + + /// + /// Bare minimum example of NetworkVariableBase derived class + /// + [Serializable] + public class MyCustomNetworkVariable : NetworkVariableBase + { + /// + /// Managed list of class instances + /// + internal List SomeDataToSynchronize = new List(); + + /// + /// Writes the complete state of the variable to the writer + /// + /// The stream to write the state to + public override void WriteField(FastBufferWriter writer) + { + // Serialize the data we need to synchronize + writer.WriteValueSafe(SomeDataToSynchronize.Count); + foreach (var dataEntry in SomeDataToSynchronize) + { + writer.WriteValueSafe(dataEntry.SomeIntData); + writer.WriteValueSafe(dataEntry.SomeFloatData); + writer.WriteValueSafe(dataEntry.SomeListOfValues.Count); + foreach (var valueItem in dataEntry.SomeListOfValues) + { + writer.WriteValueSafe(valueItem); + } + } + } + + /// + /// Reads the complete state from the reader and applies it + /// + /// The stream to read the state from + public override void ReadField(FastBufferReader reader) + { + // De-Serialize the data being synchronized + var itemsToUpdate = 0; + reader.ReadValueSafe(out itemsToUpdate); + SomeDataToSynchronize.Clear(); + for (int i = 0; i < itemsToUpdate; i++) + { + var newEntry = new SomeData(); + reader.ReadValueSafe(out newEntry.SomeIntData); + reader.ReadValueSafe(out newEntry.SomeFloatData); + var itemsCount = 0; + var tempValue = (ulong)0; + reader.ReadValueSafe(out itemsCount); + newEntry.SomeListOfValues.Clear(); + for (int j = 0; j < itemsCount; j++) + { + reader.ReadValueSafe(out tempValue); + newEntry.SomeListOfValues.Add(tempValue); + } + + SomeDataToSynchronize.Add(newEntry); + } + } + + /// + /// Used to write partial updates rather than synchronizing the full state on every change. + /// + /// The stream to write the state to + public override void WriteDelta(FastBufferWriter writer) + { + // Not implemented for this example, instead we can write the field + WriteField(writer); + } + + /// + /// Used to read partial updates rather than synchronizing the full state on every change. + /// + /// + public override void ReadDelta(FastBufferReader reader, bool keepDirtyDelta) + { + // Not implemented for this example, instead we can read the field + ReadField(reader); + } + + } + + /// + /// Bare minimum example of generic NetworkVariableBase derived class + /// + /// Generic type marker + [Serializable] + [GenerateSerializationForGenericParameter(0)] + public class MyCustomGenericNetworkVariable : NetworkVariableBase + { + /// Managed list of class instances + public List SomeDataToSynchronize = new List(); + + /// + /// Writes the complete state of the variable to the writer + /// + /// The stream to write the state to + public override void WriteField(FastBufferWriter writer) + { + // Serialize the data we need to synchronize + writer.WriteValueSafe(SomeDataToSynchronize.Count); + for (var i = 0; i < SomeDataToSynchronize.Count; ++i) + { + var dataEntry = SomeDataToSynchronize[i]; + // NetworkVariableSerialization is used for serializing generic types + NetworkVariableSerialization.Write(writer, ref dataEntry); + } + } + + /// + /// Reads the complete state from the reader and applies it + /// + /// The stream to read the state from + public override void ReadField(FastBufferReader reader) + { + // De-Serialize the data being synchronized + var itemsToUpdate = 0; + reader.ReadValueSafe(out itemsToUpdate); + SomeDataToSynchronize.Clear(); + for (int i = 0; i < itemsToUpdate; i++) + { + T newEntry = default; + // NetworkVariableSerialization is used for serializing generic types + NetworkVariableSerialization.Read(reader, ref newEntry); + SomeDataToSynchronize.Add(newEntry); + } + } + + /// + /// Used to write partial updates rather than synchronizing the full state on every change. + /// + /// The stream to write the state to + public override void WriteDelta(FastBufferWriter writer) + { + // Not implemented for this example, instead we can write the field + WriteField(writer); + } + + /// + /// Used to read partial updates rather than synchronizing the full state on every change. + /// + /// + public override void ReadDelta(FastBufferReader reader, bool keepDirtyDelta) + { + // Not implemented for this example, instead we can read the field + ReadField(reader); + } + } + + [Serializable] + internal class SomeData + { + public int SomeIntData = default; + public float SomeFloatData = default; + public List SomeListOfValues = new List(); + } + #endregion + + + internal class CustomNetworkVariableTest : NetcodeIntegrationTest + { + protected override int NumberOfClients => 2; + + private GameObject m_PrefabToSpawn; + + protected override void OnServerAndClientsCreated() + { + m_PrefabToSpawn = CreateNetworkObjectPrefab(nameof(TestMyCustomNetworkVariable)); + m_PrefabToSpawn.AddComponent(); + } + + /// + /// Validates when the authority applies a value during spawn or + /// post spawn of a newly instantiated and spawned object the value is set by the time non-authority + /// instances invoke . + /// + [UnityTest] + public IEnumerator CustomNetworkVariableCodeWorks() + { + var authority = GetAuthorityNetworkManager(); + var authorityObject = SpawnObject(m_PrefabToSpawn, authority).GetComponent(); + var authorityBehaviour = authorityObject.GetComponent(); + + yield return WaitForSpawnedOnAllOrTimeOut(authorityObject.NetworkObjectId); + AssertOnTimeout("Failed to spawn network object"); + + foreach (var networkManager in m_NetworkManagers) + { + Assert.True(networkManager.SpawnManager.SpawnedObjects.TryGetValue(authorityObject.NetworkObjectId, out var localObject)); + var testBehaviour = localObject.GetComponent(); + Assert.NotNull(testBehaviour); + Assert.AreEqual(authorityBehaviour.CustomNetworkVariable.SomeDataToSynchronize.Count, testBehaviour.CustomNetworkVariable.SomeDataToSynchronize.Count, $"[Client-{networkManager.LocalClientId}] Incorrect length found for {nameof(MyCustomNetworkVariable)}"); + Assert.AreEqual(authorityBehaviour.CustomGenericNetworkVariable.SomeDataToSynchronize, testBehaviour.CustomGenericNetworkVariable.SomeDataToSynchronize, $"[Client-{networkManager.LocalClientId}] Incorrect length found for {nameof(MyCustomNetworkVariable)}"); + } + } + } +} diff --git a/com.unity.netcode.gameobjects/Tests/Runtime/DocumentationCodeSamples/NetworkVariable/CustomNetworkVariable.cs.meta b/com.unity.netcode.gameobjects/Tests/Runtime/DocumentationCodeSamples/NetworkVariable/CustomNetworkVariable.cs.meta new file mode 100644 index 0000000000..5f186a3e56 --- /dev/null +++ b/com.unity.netcode.gameobjects/Tests/Runtime/DocumentationCodeSamples/NetworkVariable/CustomNetworkVariable.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: a4617887a8b0d46be9babcc146fcfd20 \ No newline at end of file diff --git a/com.unity.netcode.gameobjects/Tests/Runtime/DocumentationCodeSamples/NetworkVariable/CustomSerializationDocsTests.cs b/com.unity.netcode.gameobjects/Tests/Runtime/DocumentationCodeSamples/NetworkVariable/CustomSerializationDocsTests.cs new file mode 100644 index 0000000000..1e6096b187 --- /dev/null +++ b/com.unity.netcode.gameobjects/Tests/Runtime/DocumentationCodeSamples/NetworkVariable/CustomSerializationDocsTests.cs @@ -0,0 +1,213 @@ +using System.Collections; +using System.Text; +using Unity.Netcode; +using Unity.Netcode.TestHelpers.Runtime; +using UnityEngine; +using UnityEngine.TestTools; + +namespace DocumentationCodeSamples +{ + internal static class FastBufferExtensions + { + internal static void WriteValueSafe(this FastBufferWriter writer, in CustomSerializationDocsTests.Health health) + { + writer.WriteValueSafe(health.MaxHealth); + writer.WriteValueSafe(health.CurrentHealth); + } + + internal static void ReadValueSafe(this FastBufferReader reader, out CustomSerializationDocsTests.Health health) + { + reader.ReadValueSafe(out uint max); + reader.ReadValueSafe(out int current); + health = new CustomSerializationDocsTests.Health { MaxHealth = max, CurrentHealth = current }; + } + } + + + internal class CustomSerializationDocsTests : NetcodeIntegrationTest + { + #region HealthExample + public struct Health + { + public uint MaxHealth; + public int CurrentHealth; + + // Register our custom serialization on load + [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)] + public static void RegisterHealthSerialization() + { + // You can reuse the FastBufferWriter and FastBufferReader extension methods we wrote above + UserNetworkVariableSerialization.WriteValue = FastBufferExtensions.WriteValueSafe; + UserNetworkVariableSerialization.ReadValue = FastBufferExtensions.ReadValueSafe; + + // Here is where you register your custom delta handling. + UserNetworkVariableSerialization.WriteDelta = WriteDelta; + UserNetworkVariableSerialization.ReadDelta = ReadDelta; + + // You can also use lambda expressions to register functions + UserNetworkVariableSerialization.DuplicateValue = (in Health value, ref Health duplicatedValue) => { duplicatedValue = value; }; + } + + // We can use an enum to indicate which field has changed for the delta change. + // This lets us save bandwidth when only one value has changed. + // In the case of Health, we expect CurrentHealth to change much more often than MaxHealth + // Implementing WriteDelta saves us bandwidth on not sending the MaxHealth every time CurrentHealth changes. + private enum ChangeType : byte + { + MaxHealth, + CurrentHealth, + All, + } + + public static void WriteDelta(FastBufferWriter writer, in Health value, in Health previousValue) + { + if (value.MaxHealth == previousValue.MaxHealth && value.CurrentHealth != previousValue.CurrentHealth) + { + // If only our CurrentHealth has changed, we can send the CurrentHealth enum with only the updated CurrentHealth value + writer.WriteValueSafe(ChangeType.CurrentHealth); + writer.WriteValueSafe(value.CurrentHealth); + } + else if (value.CurrentHealth == previousValue.CurrentHealth && value.MaxHealth != previousValue.MaxHealth) + { + // If only our MaxHealth has changed, we can send the MaxHealth enum with only the updated MaxHealth value + writer.WriteValueSafe(ChangeType.MaxHealth); + writer.WriteValueSafe(value.MaxHealth); + } + else + { + // If both values have changed, we need to serialize both values. + writer.WriteValueSafe(ChangeType.All); + writer.WriteValueSafe(value.MaxHealth); + writer.WriteValueSafe(value.CurrentHealth); + } + } + + public static void ReadDelta(FastBufferReader reader, ref Health value) + { + // First we read what type of change we've received + reader.ReadValueSafe(out ChangeType changeType); + + // Then we read the data in our delta message, based on what type of change we've received. + switch (changeType) + { + case ChangeType.CurrentHealth: + { + reader.ReadValueSafe(out value.CurrentHealth); + break; + } + case ChangeType.MaxHealth: + { + reader.ReadValueSafe(out value.MaxHealth); + break; + } + case ChangeType.All: + { + reader.ReadValueSafe(out value.MaxHealth); + reader.ReadValueSafe(out value.CurrentHealth); + break; + } + } + } + } + #endregion + + internal class TestHealthBehaviour : NetworkBehaviour + { + internal readonly NetworkVariable HealthVar = new(); + + internal Health ReceivedFromRpc; + + [Rpc(SendTo.Everyone)] + public void SendHealthRpc(Health health) + { + ReceivedFromRpc = health; + } + } + + protected override int NumberOfClients => 1; + private GameObject m_PrefabToSpawn; + + protected override void OnServerAndClientsCreated() + { + m_PrefabToSpawn = CreateNetworkObjectPrefab(nameof(TestHealthBehaviour)); + m_PrefabToSpawn.AddComponent(); + } + + private Health m_ExpectedHealth; + private ulong m_NetworkObjectIdToTest; + + private bool m_TestingRpc; + + private bool ValidateAllAreEqual(StringBuilder errorLog) + { + foreach (var networkManager in m_NetworkManagers) + { + if (!networkManager.SpawnManager.SpawnedObjects.TryGetValue(m_NetworkObjectIdToTest, out NetworkObject localInstance)) + { + errorLog.Append($"[Client-{networkManager.LocalClientId}] SpawnedObject not found!"); + return false; + } + + var healthInstance = localInstance.GetComponent(); + if (healthInstance == null) + { + errorLog.Append($"[Client-{networkManager.LocalClientId}] Health instance is null!"); + return false; + } + + var received = m_TestingRpc ? healthInstance.ReceivedFromRpc : healthInstance.HealthVar.Value; + if (m_ExpectedHealth.MaxHealth != received.MaxHealth) + { + errorLog.Append($"[Client-{networkManager.LocalClientId}] MaxHealth values don't match! Expected {m_ExpectedHealth.MaxHealth}, Received {received.MaxHealth}"); + return false; + } + + if (m_ExpectedHealth.CurrentHealth != received.CurrentHealth) + { + errorLog.Append($"[Client-{networkManager.LocalClientId}] CurrentHealth values don't match! Expected {m_ExpectedHealth.CurrentHealth}, Received {received.CurrentHealth}"); + return false; + } + } + + return true; + } + + [UnityTest] + public IEnumerator TestHealthCode() + { + var authority = GetAuthorityNetworkManager(); + var authorityInstance = SpawnObject(m_PrefabToSpawn, authority).GetComponent(); + m_NetworkObjectIdToTest = authorityInstance.NetworkObjectId; + + yield return WaitForSpawnedOnAllOrTimeOut(authorityInstance.NetworkObjectId); + AssertOnTimeout("Failed to spawn network object"); + + var healthToTest = new Health { MaxHealth = 456, CurrentHealth = 23 }; + m_ExpectedHealth = healthToTest; + m_TestingRpc = true; + + authorityInstance.SendHealthRpc(healthToTest); + + yield return WaitForConditionOrTimeOut(ValidateAllAreEqual); + AssertOnTimeout("RPC send failed"); + + m_TestingRpc = false; + healthToTest = new Health { MaxHealth = 123, CurrentHealth = 45 }; + m_ExpectedHealth = healthToTest; + + authorityInstance.HealthVar.Value = healthToTest; + + yield return WaitForConditionOrTimeOut(ValidateAllAreEqual); + AssertOnTimeout("NetworkVariable assignment failed"); + + var current = authorityInstance.HealthVar.Value; + current.CurrentHealth -= 10; + authorityInstance.HealthVar.Value = current; + + m_ExpectedHealth = current; + + yield return WaitForConditionOrTimeOut(ValidateAllAreEqual); + AssertOnTimeout("NetworkVariable update failed"); + } + } +} diff --git a/com.unity.netcode.gameobjects/Tests/Runtime/DocumentationCodeSamples/NetworkVariable/CustomSerializationDocsTests.cs.meta b/com.unity.netcode.gameobjects/Tests/Runtime/DocumentationCodeSamples/NetworkVariable/CustomSerializationDocsTests.cs.meta new file mode 100644 index 0000000000..1588a4d111 --- /dev/null +++ b/com.unity.netcode.gameobjects/Tests/Runtime/DocumentationCodeSamples/NetworkVariable/CustomSerializationDocsTests.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 22820b0916e341ddbcadc50030dc5f60 +timeCreated: 1780100433 \ No newline at end of file diff --git a/com.unity.netcode.gameobjects/Tests/Runtime/DocumentationCodeSamples/README.md b/com.unity.netcode.gameobjects/Tests/Runtime/DocumentationCodeSamples/README.md new file mode 100644 index 0000000000..92cd488e8e --- /dev/null +++ b/com.unity.netcode.gameobjects/Tests/Runtime/DocumentationCodeSamples/README.md @@ -0,0 +1,33 @@ +# Code documentation + +Use this folder for any code examples that we want to test to ensure the runtime functionality isn't broken + +Any code snippets that are small enough that you only want to check that they compile should be in [Editor tests](../../Editor/Documentation/README.md). + +To embed code in documentation, use the following tag + +```md +[!code-cs[](../../Tests/Runtime/DocumentationCodeSamples/.cs#SomeRegionName)] +``` + +With the code formatted like this + +```cs +namespace DocumentationCodeSamples +{ + internal MyTestClass : NetcodeIntegrationTest + { + #region SomeRegionName + // All the code in this region block will be embedded without indentation in the docs. + #endregion + + protected override int NumberOfClients => 1; + + [UnityTest] + public IEnumerator TestOfDocumentationCode() + { + ... + } + } +} +``` diff --git a/com.unity.netcode.gameobjects/Tests/Runtime/DocumentationCodeSamples/README.md.meta b/com.unity.netcode.gameobjects/Tests/Runtime/DocumentationCodeSamples/README.md.meta new file mode 100644 index 0000000000..4f45c6f105 --- /dev/null +++ b/com.unity.netcode.gameobjects/Tests/Runtime/DocumentationCodeSamples/README.md.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 8a8cfc9d633474efb95052169e9fcb50 +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: From 413c5a392cbaadd1ebdd5c87b901d0d83d4f32b6 Mon Sep 17 00:00:00 2001 From: Emma Date: Mon, 6 Jul 2026 20:02:41 -0400 Subject: [PATCH 23/26] fix: NullReferenceExceptions in spawn path (#4067) * fix: NullReferenceExceptions in spawn path * Update NetcodeIntegrationTest * Update CHANGELOG.md * put back serialization code --- com.unity.netcode.gameobjects/CHANGELOG.md | 1 + .../Connection/NetworkConnectionManager.cs | 2 +- .../Runtime/Core/NetworkObject.cs | 25 ++- .../Runtime/Messaging/ILPPMessageProvider.cs | 1 + .../Messages/ConnectionApprovedMessage.cs | 4 +- .../Messaging/Messages/CreateObjectMessage.cs | 59 ++--- .../Runtime/SceneManagement/SceneEventData.cs | 21 +- .../Runtime/Spawning/NetworkSpawnManager.cs | 10 +- .../NetworkObjectSynchronizationTests.cs | 204 +++++++----------- ...etworkPrefabHandlerSynchronizationTests.cs | 88 ++++++++ ...kPrefabHandlerSynchronizationTests.cs.meta | 3 + .../TestHelpers/NetcodeIntegrationTest.cs | 2 +- 12 files changed, 234 insertions(+), 186 deletions(-) create mode 100644 com.unity.netcode.gameobjects/Tests/Runtime/Prefabs/NetworkPrefabHandlerSynchronizationTests.cs create mode 100644 com.unity.netcode.gameobjects/Tests/Runtime/Prefabs/NetworkPrefabHandlerSynchronizationTests.cs.meta diff --git a/com.unity.netcode.gameobjects/CHANGELOG.md b/com.unity.netcode.gameobjects/CHANGELOG.md index 0c5e47d1a0..5374068ba0 100644 --- a/com.unity.netcode.gameobjects/CHANGELOG.md +++ b/com.unity.netcode.gameobjects/CHANGELOG.md @@ -22,6 +22,7 @@ Additional documentation and release notes are available at [Multiplayer Documen ### Fixed +- Issue where a NullReferenceException was thrown when a non-authority failed to spawn a NetworkObject. (#4067) - Issue when FastBufferReader is attempting to read a string and all or a portion of the character count has already been read by user script, it could read a character length that results in a negative byte length which could result in an editor crash. (#4052) - Issue where NetworkRigidbodyBase was not applying rotation correctly when using Rigidbody2D. (#4012) - Issue where NetworkRigidbodyBase was always checking the 3D rigid body's interpolation mode when determining if it is kinematic and needs to put the rigid body to sleep and then switch to interpolation. (#4012) diff --git a/com.unity.netcode.gameobjects/Runtime/Connection/NetworkConnectionManager.cs b/com.unity.netcode.gameobjects/Runtime/Connection/NetworkConnectionManager.cs index cc44bd8783..dd22801412 100644 --- a/com.unity.netcode.gameobjects/Runtime/Connection/NetworkConnectionManager.cs +++ b/com.unity.netcode.gameobjects/Runtime/Connection/NetworkConnectionManager.cs @@ -1218,7 +1218,7 @@ internal void ApprovedPlayerSpawn(ulong clientId, uint playerPrefabHash) var message = new CreateObjectMessage { - ObjectInfo = ConnectedClients[clientId].PlayerObject.Serialize(clientPair.Key), + ObjectInfo = ConnectedClients[clientId].PlayerObject.SerializeSpawnedObject(clientPair.Key), IncludesSerializedObject = true, }; diff --git a/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs b/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs index 3423c05a07..6a9397e48e 100644 --- a/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs +++ b/com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Runtime.CompilerServices; using System.Text; @@ -3369,7 +3370,12 @@ internal void SynchronizeNetworkBehaviours(ref BufferSerializer serializer } } - internal SerializedObject Serialize(ulong targetClientId = NetworkManager.ServerClientId, bool syncObservers = false) + /// + /// Creates a on an authority client. + /// Used to synchronize state to a non-authority client. + /// + /// This function is the authority mirror of + internal SerializedObject SerializeSpawnedObject(ulong targetClientId = NetworkManager.ServerClientId, bool syncObservers = false) { var obj = new SerializedObject { @@ -3444,15 +3450,17 @@ internal SerializedObject Serialize(ulong targetClientId = NetworkManager.Server } /// - /// Used to deserialize a serialized which occurs - /// when the client is approved or during a scene transition + /// Does a non-authority local spawn of a given . + /// This occurs when the client is approved, a new object is spawned by an authority, or during a scene transition. /// - /// Deserialized scene object data - /// FastBufferReader for the NetworkVariable data - /// NetworkManager instance + /// This function is the non-authority mirror of + /// Deserialized data received from the authority for this + /// FastBufferReader for any additional data sent with this object on spawn. + /// NetworkManager instance. /// will be true if invoked by CreateObjectMessage /// The deserialized NetworkObject or null if deserialization failed - internal static NetworkObject Deserialize(in SerializedObject serializedObject, FastBufferReader reader, NetworkManager networkManager, bool invokedByMessage = false) + [return: MaybeNull] + internal static NetworkObject DeserializeAndSpawnObject(in SerializedObject serializedObject, FastBufferReader reader, NetworkManager networkManager, bool invokedByMessage = false) { var endOfSynchronizationData = reader.Position + serializedObject.SynchronizationDataSize; @@ -3479,7 +3487,8 @@ internal static NetworkObject Deserialize(in SerializedObject serializedObject, { if (networkManager.LogLevel <= LogLevel.Normal) { - NetworkLog.LogWarning($"[{networkObject.name}][Deserialize][{nameof(NetworkBehaviour)}Synchronization][Size mismatch] Expected: {endOfSynchronizationData} Currently At: {reader.Position}!"); + var networkObjectName = networkObject != null ? networkObject.name : "null"; + NetworkLog.LogWarning($"[{networkObjectName}][Deserialize][{nameof(NetworkBehaviour)}Synchronization][Size mismatch] Expected: {endOfSynchronizationData} Currently At: {reader.Position}!"); } reader.Seek(endOfSynchronizationData); } diff --git a/com.unity.netcode.gameobjects/Runtime/Messaging/ILPPMessageProvider.cs b/com.unity.netcode.gameobjects/Runtime/Messaging/ILPPMessageProvider.cs index 21413a4fae..8d8379dc05 100644 --- a/com.unity.netcode.gameobjects/Runtime/Messaging/ILPPMessageProvider.cs +++ b/com.unity.netcode.gameobjects/Runtime/Messaging/ILPPMessageProvider.cs @@ -148,6 +148,7 @@ internal static Dictionary GetMessageTypesMap() [InitializeOnLoadMethod] public static void NotifyOnPlayStateChange() { + EditorApplication.playModeStateChanged -= OnPlayModeStateChanged; EditorApplication.playModeStateChanged += OnPlayModeStateChanged; } diff --git a/com.unity.netcode.gameobjects/Runtime/Messaging/Messages/ConnectionApprovedMessage.cs b/com.unity.netcode.gameobjects/Runtime/Messaging/Messages/ConnectionApprovedMessage.cs index 17669a335f..2a7eee8be7 100644 --- a/com.unity.netcode.gameobjects/Runtime/Messaging/Messages/ConnectionApprovedMessage.cs +++ b/com.unity.netcode.gameobjects/Runtime/Messaging/Messages/ConnectionApprovedMessage.cs @@ -160,7 +160,7 @@ public void Serialize(FastBufferWriter writer, int targetVersion) { sobj.AddObserver(OwnerClientId); // In distributed authority mode, we send the currently known observers of each NetworkObject to the client being synchronized. - var serializedObject = sobj.Serialize(OwnerClientId, IsDistributedAuthority); + var serializedObject = sobj.SerializeSpawnedObject(OwnerClientId, IsDistributedAuthority); serializedObject.Serialize(writer); ++sceneObjectCount; } @@ -344,7 +344,7 @@ public void Handle(ref NetworkContext context) { var serializedObject = new NetworkObject.SerializedObject(); serializedObject.Deserialize(m_ReceivedSceneObjectData); - NetworkObject.Deserialize(serializedObject, m_ReceivedSceneObjectData, networkManager); + NetworkObject.DeserializeAndSpawnObject(serializedObject, m_ReceivedSceneObjectData, networkManager); } if (networkManager.DistributedAuthorityMode && networkManager.AutoSpawnPlayerPrefabClientSide) diff --git a/com.unity.netcode.gameobjects/Runtime/Messaging/Messages/CreateObjectMessage.cs b/com.unity.netcode.gameobjects/Runtime/Messaging/Messages/CreateObjectMessage.cs index b8f8af78fb..a5f3039251 100644 --- a/com.unity.netcode.gameobjects/Runtime/Messaging/Messages/CreateObjectMessage.cs +++ b/com.unity.netcode.gameobjects/Runtime/Messaging/Messages/CreateObjectMessage.cs @@ -1,5 +1,6 @@ using System.Linq; using System.Runtime.CompilerServices; +using Unity.Netcode.Logging; namespace Unity.Netcode { @@ -171,7 +172,12 @@ internal static void CreateObject(ref NetworkManager networkManager, ulong sende { if (!networkManager.DistributedAuthorityMode) { - networkObject = NetworkObject.Deserialize(serializedObject, networkVariableData, networkManager); + networkObject = NetworkObject.DeserializeAndSpawnObject(serializedObject, networkVariableData, networkManager); + if (networkObject == null) + { + networkManager.Log.ErrorServer(new Context(LogLevel.Developer, $"Failed to deserialize {nameof(NetworkObject)}.").AddInfo(nameof(NetworkObject.GlobalObjectIdHash), serializedObject.Hash).AddInfo(nameof(NetworkObject.NetworkObjectId), serializedObject.NetworkObjectId)); + return; + } } else { @@ -179,25 +185,27 @@ internal static void CreateObject(ref NetworkManager networkManager, ulong sende var hasNewObserverIdList = newObserverIds != null && newObserverIds.Length > 0; // Depending upon visibility of the NetworkObject and the client in question, it could be that // this client already has visibility of this NetworkObject - if (networkManager.SpawnManager.SpawnedObjects.ContainsKey(serializedObject.NetworkObjectId)) + if (networkManager.SpawnManager.SpawnedObjects.TryGetValue(serializedObject.NetworkObjectId, out networkObject)) { - // If so, then just get the local instance - networkObject = networkManager.SpawnManager.SpawnedObjects[serializedObject.NetworkObjectId]; - // This should not happen, logging error just in case if (hasNewObserverIdList && newObserverIds.Contains(networkManager.LocalClientId)) { NetworkLog.LogErrorServer($"[{nameof(CreateObjectMessage)}][Duplicate-Broadcast] Detected duplicated object creation for {serializedObject.NetworkObjectId}!"); } - else // Trap to make sure the owner is not receiving any messages it sent - if (networkManager.CMBServiceConnection && networkManager.LocalClientId == networkObject.OwnerClientId) - { - NetworkLog.LogWarning($"[{nameof(CreateObjectMessage)}][Client-{networkManager.LocalClientId}][Duplicate-CreateObjectMessage][Client Is Owner] Detected duplicated object creation for {networkObject.name}-{serializedObject.NetworkObjectId}!"); - } + // Trap to make sure the owner is not receiving any messages it sent + else if (networkManager.CMBServiceConnection && networkManager.LocalClientId == networkObject.OwnerClientId) + { + NetworkLog.LogWarning($"[{nameof(CreateObjectMessage)}][Client-{networkManager.LocalClientId}][Duplicate-CreateObjectMessage][Client Is Owner] Detected duplicated object creation for {networkObject.name}-{serializedObject.NetworkObjectId}!"); + } } else { - networkObject = NetworkObject.Deserialize(serializedObject, networkVariableData, networkManager, true); + networkObject = NetworkObject.DeserializeAndSpawnObject(serializedObject, networkVariableData, networkManager, true); + if (networkObject == null) + { + networkManager.Log.ErrorServer(new Context(LogLevel.Developer, $"Failed to deserialize {nameof(NetworkObject)}.").AddInfo(nameof(NetworkObject.GlobalObjectIdHash), serializedObject.Hash).AddInfo(nameof(NetworkObject.NetworkObjectId), serializedObject.NetworkObjectId)); + return; + } } // DA - NGO CMB SERVICE NOTES: @@ -214,27 +222,6 @@ internal static void CreateObject(ref NetworkManager networkManager, ulong sende // Mock CMB Service and forward to all clients if (networkManager.DAHost) { - // DA - NGO CMB SERVICE NOTES: - // (*** See above notes fist ***) - // If it is a player object freshly spawning and one or more clients all connect at the exact same time (i.e. received on effectively - // the same frame), then we need to check the observers list to make sure all players are visible upon first spawning. At a later date, - // for area of interest we will need to have some form of follow up "observer update" message to cull out players not within each - // player's AOI. - if (networkObject.IsPlayerObject && hasNewObserverIdList && clientList.Count != observerIds.Length) - { - // For same-frame newly spawned players that might not be aware of all other players, update the player's observer - // list. - observerIds = clientList.ToArray(); - } - - var createObjectMessage = new CreateObjectMessage() - { - ObjectInfo = serializedObject, - m_ReceivedNetworkVariableData = networkVariableData, - ObserverIds = hasObserverIdList ? observerIds : null, - NetworkObjectId = networkObject.NetworkObjectId, - IncludesSerializedObject = true, - }; foreach (var clientId in clientList) { // DA - NGO CMB SERVICE NOTES: @@ -250,16 +237,12 @@ internal static void CreateObject(ref NetworkManager networkManager, ulong sende // If this included a list of new observers and the targeted clientId is one of the observers, then send the serialized data. // Otherwise, the targeted clientId has already has visibility (i.e. it is already spawned) and so just send the updated // observers list to that client's instance. - createObjectMessage.IncludesSerializedObject = hasNewObserverIdList && newObserverIds.Contains(clientId); - networkManager.SpawnManager.SendSpawnCallForObject(clientId, networkObject); } } } - if (networkObject != null) - { - networkManager.NetworkMetrics.TrackObjectSpawnReceived(senderId, networkObject, messageSize); - } + + networkManager.NetworkMetrics.TrackObjectSpawnReceived(senderId, networkObject, messageSize); } catch (System.Exception ex) { diff --git a/com.unity.netcode.gameobjects/Runtime/SceneManagement/SceneEventData.cs b/com.unity.netcode.gameobjects/Runtime/SceneManagement/SceneEventData.cs index 4840944117..2575bd6bfd 100644 --- a/com.unity.netcode.gameobjects/Runtime/SceneManagement/SceneEventData.cs +++ b/com.unity.netcode.gameobjects/Runtime/SceneManagement/SceneEventData.cs @@ -622,7 +622,7 @@ private void WriteSceneSynchronizationData(FastBufferWriter writer) { var noStart = writer.Position; // In distributed authority mode, we send the currently known observers of each NetworkObject to the client being synchronized. - var serializedObject = networkObject.Serialize(TargetClientId, distributedAuthority); + var serializedObject = networkObject.SerializeSpawnedObject(TargetClientId, distributedAuthority); serializedObject.Serialize(writer); var noStop = writer.Position; @@ -698,7 +698,7 @@ private void SerializeScenePlacedObjects(FastBufferWriter writer) foreach (var objectToSync in m_NetworkObjectsSync) { // Serialize the NetworkObject - var serializedObject = objectToSync.Serialize(TargetClientId, distributedAuthority); + var serializedObject = objectToSync.SerializeSpawnedObject(TargetClientId, distributedAuthority); serializedObject.Serialize(writer); numberOfObjects++; } @@ -875,9 +875,9 @@ internal void DeserializeScenePlacedObjects() m_NetworkManager.SceneManager.SetTheSceneBeingSynchronized(serializedObject.NetworkSceneHandle); } - var networkObject = NetworkObject.Deserialize(serializedObject, m_InternalBuffer, m_NetworkManager); + var networkObject = NetworkObject.DeserializeAndSpawnObject(serializedObject, m_InternalBuffer, m_NetworkManager); - if (serializedObject.IsSceneObject) + if (serializedObject.IsSceneObject && networkObject != null) { sceneObjects.Add(networkObject); } @@ -1101,7 +1101,11 @@ internal void SynchronizeSceneNetworkObjects(NetworkManager networkManager) { m_NetworkManager.SceneManager.SetTheSceneBeingSynchronized(serializedObject.NetworkSceneHandle); } - var spawnedNetworkObject = NetworkObject.Deserialize(serializedObject, m_InternalBuffer, networkManager); + var spawnedNetworkObject = NetworkObject.DeserializeAndSpawnObject(serializedObject, m_InternalBuffer, networkManager); + if (spawnedNetworkObject == null) + { + continue; + } var noStop = m_InternalBuffer.Position; if (EnableSerializationLogs) @@ -1110,12 +1114,9 @@ internal void SynchronizeSceneNetworkObjects(NetworkManager networkManager) LogArray(m_InternalBuffer.ToArray(), noStart, noStop, builder); } // If we failed to deserialize the NetworkObject then don't add null to the list - if (spawnedNetworkObject != null) + if (!m_NetworkObjectsSync.Contains(spawnedNetworkObject)) { - if (!m_NetworkObjectsSync.Contains(spawnedNetworkObject)) - { - m_NetworkObjectsSync.Add(spawnedNetworkObject); - } + m_NetworkObjectsSync.Add(spawnedNetworkObject); } } if (EnableSerializationLogs) diff --git a/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkSpawnManager.cs b/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkSpawnManager.cs index 38711e155c..2f8ae30d03 100644 --- a/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkSpawnManager.cs +++ b/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkSpawnManager.cs @@ -1165,13 +1165,13 @@ internal bool AuthorityLocalSpawn([NotNull] NetworkObject networkObject, ulong n /// /// Only spawn non-authority instances should invoke this. /// This is invoked to instantiate an authority spawned , and - /// is only invoked by: + /// is only invoked by: /// /// - /// IMPORTANT: Pre spawn methods need to be invoked from within . + /// IMPORTANT: Pre spawn methods need to be invoked from within . /// /// boolean indicating whether the spawn succeeded - internal bool NonAuthorityLocalSpawn(in NetworkObject.SerializedObject serializedObject, out NetworkObject networkObject, FastBufferReader reader, bool destroyWithScene) + internal bool NonAuthorityLocalSpawn(in NetworkObject.SerializedObject serializedObject, [MaybeNullWhen(false)] out NetworkObject networkObject, FastBufferReader reader, bool destroyWithScene) { if (SpawnedObjects.ContainsKey(serializedObject.NetworkObjectId)) { @@ -1368,7 +1368,7 @@ internal void SendSpawnCallForObject(ulong clientId, NetworkObject networkObject } var message = new CreateObjectMessage { - ObjectInfo = networkObject.Serialize(clientId, NetworkManager.DistributedAuthorityMode), + ObjectInfo = networkObject.SerializeSpawnedObject(clientId, NetworkManager.DistributedAuthorityMode), IncludesSerializedObject = true, UpdateObservers = NetworkManager.DistributedAuthorityMode, ObserverIds = NetworkManager.DistributedAuthorityMode ? networkObject.Observers.ToArray() : null, @@ -1394,7 +1394,7 @@ internal void SendSpawnCallForObserverUpdate(ulong[] newObservers, NetworkObject var message = new CreateObjectMessage { - ObjectInfo = networkObject.Serialize(), + ObjectInfo = networkObject.SerializeSpawnedObject(), ObserverIds = networkObject.Observers.ToArray(), NewObserverIds = newObservers.ToArray(), IncludesSerializedObject = true, diff --git a/com.unity.netcode.gameobjects/Tests/Runtime/NetworkObject/NetworkObjectSynchronizationTests.cs b/com.unity.netcode.gameobjects/Tests/Runtime/NetworkObject/NetworkObjectSynchronizationTests.cs index 6224c6c627..7535e4ffce 100644 --- a/com.unity.netcode.gameobjects/Tests/Runtime/NetworkObject/NetworkObjectSynchronizationTests.cs +++ b/com.unity.netcode.gameobjects/Tests/Runtime/NetworkObject/NetworkObjectSynchronizationTests.cs @@ -8,7 +8,6 @@ namespace Unity.Netcode.RuntimeTests { - [UnityPlatform(exclude = new[] { RuntimePlatform.IPhonePlayer })] // Ignored test tracked in MTT-14172 [TestFixture(VariableLengthSafety.DisableNetVarSafety, HostOrServer.DAHost)] [TestFixture(VariableLengthSafety.DisableNetVarSafety, HostOrServer.Host)] @@ -28,12 +27,6 @@ internal class NetworkObjectSynchronizationTests : NetcodeIntegrationTest private LogLevel m_CurrentLogLevel; - // TODO: [CmbServiceTests] Adapt to run with the service - protected override bool UseCMBService() - { - return false; - } - public enum VariableLengthSafety { DisableNetVarSafety, @@ -57,15 +50,15 @@ protected override void OnCreatePlayerPrefab() protected override void OnServerAndClientsCreated() { - + var authority = GetAuthorityNetworkManager(); // Set the NetworkVariable Safety Check setting - m_ServerNetworkManager.NetworkConfig.EnsureNetworkVariableLengthSafety = m_VariableLengthSafety == VariableLengthSafety.EnabledNetVarSafety; + authority.NetworkConfig.EnsureNetworkVariableLengthSafety = m_VariableLengthSafety == VariableLengthSafety.EnabledNetVarSafety; // Ignore the errors generated during this test (they are expected) - m_ServerNetworkManager.LogLevel = LogLevel.Nothing; + authority.LogLevel = LogLevel.Nothing; // Disable forcing the same prefabs to avoid failed connections - m_ServerNetworkManager.NetworkConfig.ForceSamePrefabs = false; + authority.NetworkConfig.ForceSamePrefabs = false; // Create the valid network prefab m_NetworkPrefab = CreateNetworkObjectPrefab("ValidObject"); @@ -104,8 +97,9 @@ protected override void OnNewClientCreated(NetworkManager networkManager) public IEnumerator NetworkObjectDeserializationFailure() { m_CurrentLogLevel = LogLevel.Nothing; - var validSpawnedNetworkObjects = new List(); + var authoritySpawnedNetworkObjects = new List(); NetworkBehaviourWithNetworkVariables.ResetSpawnCount(); + var authority = GetAuthorityNetworkManager(); // Spawn NetworkObjects on the server side with half of them being the // invalid network prefabs to simulate NetworkObject synchronization failure @@ -113,47 +107,29 @@ public IEnumerator NetworkObjectDeserializationFailure() { if (i % 2 == 0) { - SpawnObject(m_InValidNetworkPrefab, m_ServerNetworkManager); + SpawnObject(m_InValidNetworkPrefab, authority); } else { // Keep track of the prefabs that should successfully spawn on the client side - validSpawnedNetworkObjects.Add(SpawnObject(m_NetworkPrefab, m_ServerNetworkManager)); + var instance = SpawnObject(m_NetworkPrefab, authority); + authoritySpawnedNetworkObjects.Add(instance.GetComponent()); } } // Assure the server-side spawned all NetworkObjects - yield return WaitForConditionOrTimeOut(() => NetworkBehaviourWithNetworkVariables.ServerSpawnCount == k_NumberToSpawn); + yield return WaitForConditionOrTimeOut(() => NetworkBehaviourWithNetworkVariables.AuthoritySpawnCount == k_NumberToSpawn); // Now spawn and connect a client that will fail to spawn half of the NetworkObjects spawned - yield return CreateAndStartNewClient(); + var newClient = CreateNewClient(); + yield return StartClient(newClient); if (m_UseHost) { - var delayCounter = 0; - while (m_ClientNetworkManagers.Length == 0) - { - delayCounter++; - Assert.True(delayCounter < 30, "TimeOut waiting for client to spawn!"); - yield return s_DefaultWaitForTick; - } - delayCounter = 0; - while (!m_PlayerNetworkObjects[m_ServerNetworkManager.LocalClientId].ContainsKey(m_ClientNetworkManagers[0].LocalClientId)) - { - delayCounter++; - if (delayCounter >= 30) - { - VerboseDebug("Trap!"); - } - Assert.True(delayCounter < 30, "TimeOut waiting for client to spawn!"); - yield return s_DefaultWaitForTick; - } - - - var serverSideClientPlayerComponent = m_PlayerNetworkObjects[m_ServerNetworkManager.LocalClientId][m_ClientNetworkManagers[0].LocalClientId].GetComponent(); - var serverSideHostPlayerComponent = m_ServerNetworkManager.LocalClient.PlayerObject.GetComponent(); - var clientSidePlayerComponent = m_ClientNetworkManagers[0].LocalClient.PlayerObject.GetComponent(); - var clientSideHostPlayerComponent = m_PlayerNetworkObjects[m_ClientNetworkManagers[0].LocalClientId][m_ServerNetworkManager.LocalClientId].GetComponent(); + var serverSideClientPlayerComponent = m_PlayerNetworkObjects[authority.LocalClientId][newClient.LocalClientId].GetComponent(); + var serverSideHostPlayerComponent = authority.LocalClient.PlayerObject.GetComponent(); + var clientSidePlayerComponent = newClient.LocalClient.PlayerObject.GetComponent(); + var clientSideHostPlayerComponent = m_PlayerNetworkObjects[newClient.LocalClientId][authority.LocalClientId].GetComponent(); var modeText = m_DistributedAuthority ? "owner" : "server"; // Validate that the client side player values match the server side value of the client's player Assert.IsTrue(serverSideClientPlayerComponent.NetworkVariableData1.Value == clientSidePlayerComponent.NetworkVariableData1.Value, @@ -192,16 +168,15 @@ public IEnumerator NetworkObjectDeserializationFailure() else { // Spawn and connect another client when running as a server - yield return CreateAndStartNewClient(); - yield return WaitForConditionOrTimeOut(() => m_PlayerNetworkObjects[2].Count > 1); - AssertOnTimeout($"Timed out waiting for second client to have access to the first client's cloned player object!"); + var secondClient = CreateNewClient(); + yield return StartClient(secondClient); - var clientSide1PlayerComponent = m_ClientNetworkManagers[0].LocalClient.PlayerObject.GetComponent(); - var clientSide2Player1Clone = m_PlayerNetworkObjects[2][clientSide1PlayerComponent.OwnerClientId].GetComponent(); + var clientSide1PlayerComponent = newClient.LocalClient.PlayerObject.GetComponent(); + var clientSide2Player1Clone = m_PlayerNetworkObjects[secondClient.LocalClientId][clientSide1PlayerComponent.OwnerClientId].GetComponent(); var clientOneId = clientSide1PlayerComponent.OwnerClientId; - var clientSide2PlayerComponent = m_ClientNetworkManagers[1].LocalClient.PlayerObject.GetComponent(); - var clientSide1Player2Clone = m_PlayerNetworkObjects[1][clientSide2PlayerComponent.OwnerClientId].GetComponent(); + var clientSide2PlayerComponent = secondClient.LocalClient.PlayerObject.GetComponent(); + var clientSide1Player2Clone = m_PlayerNetworkObjects[newClient.LocalClientId][clientSide2PlayerComponent.OwnerClientId].GetComponent(); var clientTwoId = clientSide2PlayerComponent.OwnerClientId; // Validate that client one's 2nd and 4th NetworkVariables for the local and clone instances match and the other two do not @@ -244,75 +219,57 @@ public IEnumerator NetworkObjectDeserializationFailure() } } - // DANGO-TODO: This scenario is only possible to do if we add a DA-Server to mock the CMB Service or we integrate the CMB Service AND we have updated NetworkVariable permissions - // to only allow the service to write. For now, we will skip this validation for distributed authority - if (!m_DistributedAuthority) + // Now validate all of the NetworkVariable values match to assure everything synchronized properly + foreach (var spawnedObject in authoritySpawnedNetworkObjects) { - // Now validate all of the NetworkVariable values match to assure everything synchronized properly - foreach (var spawnedObject in validSpawnedNetworkObjects) + foreach (var networkManager in m_NetworkManagers) { - foreach (var clientNetworkManager in m_ClientNetworkManagers) + if (networkManager == authority) { - //Validate that the connected client has spawned all of the instances that shouldn't have failed. - var clientSideNetworkObjects = s_GlobalNetworkObjects[clientNetworkManager.LocalClientId]; + continue; + } + //Validate that the connected client has spawned all of the instances that shouldn't have failed. + var clientSideNetworkObjects = s_GlobalNetworkObjects[networkManager.LocalClientId]; - Assert.IsTrue(NetworkBehaviourWithNetworkVariables.ClientSpawnCount[clientNetworkManager.LocalClientId] == validSpawnedNetworkObjects.Count, $"Client-{clientNetworkManager.LocalClientId} spawned " + - $"({NetworkBehaviourWithNetworkVariables.ClientSpawnCount}) {nameof(NetworkObject)}s but the expected number of {nameof(NetworkObject)}s should have been ({validSpawnedNetworkObjects.Count})!"); + Assert.IsTrue(NetworkBehaviourWithNetworkVariables.NonAuthoritySpawnCount[networkManager.LocalClientId] == authoritySpawnedNetworkObjects.Count, $"Client-{networkManager.LocalClientId} spawned " + + $"({NetworkBehaviourWithNetworkVariables.NonAuthoritySpawnCount}) {nameof(NetworkObject)}s but the expected number of {nameof(NetworkObject)}s should have been ({authoritySpawnedNetworkObjects.Count})!"); - var spawnedNetworkObject = spawnedObject.GetComponent(); - Assert.IsTrue(clientSideNetworkObjects.ContainsKey(spawnedNetworkObject.NetworkObjectId), $"Failed to find valid spawned {nameof(NetworkObject)} on the client-side with a " + - $"{nameof(NetworkObject.NetworkObjectId)} of {spawnedNetworkObject.NetworkObjectId}"); + Assert.IsTrue(clientSideNetworkObjects.ContainsKey(spawnedObject.NetworkObjectId), $"Failed to find valid spawned {nameof(NetworkObject)} on the client-side with a " + + $"{nameof(NetworkObject.NetworkObjectId)} of {spawnedObject.NetworkObjectId}"); - var clientSideObject = clientSideNetworkObjects[spawnedNetworkObject.NetworkObjectId]; - Assert.IsTrue(clientSideObject.NetworkManager == clientNetworkManager, $"Client-side object {clientSideObject}'s {nameof(NetworkManager)} is not valid!"); + var clientSideObject = clientSideNetworkObjects[spawnedObject.NetworkObjectId]; + Assert.IsTrue(clientSideObject.NetworkManager == networkManager, $"Client-side object {clientSideObject}'s {nameof(NetworkManager)} is not valid!"); - ValidateNetworkBehaviourWithNetworkVariables(spawnedNetworkObject, clientSideObject); - } + ValidateNetworkBehaviourWithNetworkVariables(spawnedObject, clientSideObject); } } } - private void ValidateNetworkBehaviourWithNetworkVariables(NetworkObject serverSideNetworkObject, NetworkObject clientSideNetworkObject) + private void ValidateNetworkBehaviourWithNetworkVariables(NetworkObject authorityNetworkObject, NetworkObject nonAuthorityNetworkObject) { - var serverSideComponent = serverSideNetworkObject.GetComponent(); - var clientSideComponent = clientSideNetworkObject.GetComponent(); + var authorityComponent = authorityNetworkObject.GetComponent(); + var nonAuthorityComponent = nonAuthorityNetworkObject.GetComponent(); string netVarName1 = nameof(NetworkBehaviourWithNetworkVariables.NetworkVariableData1); string netVarName2 = nameof(NetworkBehaviourWithNetworkVariables.NetworkVariableData1); string netVarName3 = nameof(NetworkBehaviourWithNetworkVariables.NetworkVariableData1); string netVarName4 = nameof(NetworkBehaviourWithNetworkVariables.NetworkVariableData1); - Assert.IsTrue(serverSideComponent.NetworkVariableData1.Count == clientSideComponent.NetworkVariableData1.Count, $"[{serverSideComponent.name}:{netVarName1}] Server side {nameof(NetworkList)} " + - $"count ({serverSideComponent.NetworkVariableData1.Count}) does not match the client side {nameof(NetworkList)} count ({clientSideComponent.NetworkVariableData1.Count})!"); + Assert.IsTrue(authorityComponent.NetworkVariableData1.Count == nonAuthorityComponent.NetworkVariableData1.Count, $"[{authorityComponent.name}:{netVarName1}] Server side {nameof(NetworkList)} " + + $"count ({authorityComponent.NetworkVariableData1.Count}) does not match the client side {nameof(NetworkList)} count ({nonAuthorityComponent.NetworkVariableData1.Count})!"); - for (int i = 0; i < serverSideComponent.NetworkVariableData1.Count; i++) + for (int i = 0; i < authorityComponent.NetworkVariableData1.Count; i++) { - Assert.IsTrue(serverSideComponent.NetworkVariableData1[i] == clientSideComponent.NetworkVariableData1[i], $"[{serverSideComponent.name}:{netVarName1}][Index:{i}] Server side instance value " + - $"({serverSideComponent.NetworkVariableData1[i]}) does not match the client side instance value ({clientSideComponent.NetworkVariableData1[i]})!"); + Assert.IsTrue(authorityComponent.NetworkVariableData1[i] == nonAuthorityComponent.NetworkVariableData1[i], $"[{authorityComponent.name}:{netVarName1}][Index:{i}] Server side instance value " + + $"({authorityComponent.NetworkVariableData1[i]}) does not match the client side instance value ({nonAuthorityComponent.NetworkVariableData1[i]})!"); } - Assert.IsTrue(serverSideComponent.NetworkVariableData2.Value == clientSideComponent.NetworkVariableData2.Value, $"[{serverSideComponent.name}:{netVarName2}] Server side instance value ({serverSideComponent.NetworkVariableData2.Value}) " + - $"does not match the client side instance value ({clientSideComponent.NetworkVariableData2.Value})!"); - Assert.IsTrue(serverSideComponent.NetworkVariableData3.Value == clientSideComponent.NetworkVariableData3.Value, $"[{serverSideComponent.name}:{netVarName3}] Server side instance value ({serverSideComponent.NetworkVariableData3.Value}) " + - $"does not match the client side instance value ({clientSideComponent.NetworkVariableData3.Value})!"); - Assert.IsTrue(serverSideComponent.NetworkVariableData4.Value == clientSideComponent.NetworkVariableData4.Value, $"[{serverSideComponent.name}:{netVarName4}] Server side instance value ({serverSideComponent.NetworkVariableData4.Value}) " + - $"does not match the client side instance value ({clientSideComponent.NetworkVariableData4.Value})!"); - } - - - private bool ClientSpawnedNetworkObjects(List spawnedObjectList) - { - var clientSideNetworkObjects = s_GlobalNetworkObjects[m_ClientNetworkManagers[0].LocalClientId]; - - foreach (var spawnedObject in spawnedObjectList) - { - var serverSideSpawnedNetworkObject = spawnedObject.GetComponent(); - if (!clientSideNetworkObjects.ContainsKey(serverSideSpawnedNetworkObject.NetworkObjectId)) - { - return false; - } - } - return true; + Assert.IsTrue(authorityComponent.NetworkVariableData2.Value == nonAuthorityComponent.NetworkVariableData2.Value, $"[{authorityComponent.name}:{netVarName2}] Server side instance value ({authorityComponent.NetworkVariableData2.Value}) " + + $"does not match the client side instance value ({nonAuthorityComponent.NetworkVariableData2.Value})!"); + Assert.IsTrue(authorityComponent.NetworkVariableData3.Value == nonAuthorityComponent.NetworkVariableData3.Value, $"[{authorityComponent.name}:{netVarName3}] Server side instance value ({authorityComponent.NetworkVariableData3.Value}) " + + $"does not match the client side instance value ({nonAuthorityComponent.NetworkVariableData3.Value})!"); + Assert.IsTrue(authorityComponent.NetworkVariableData4.Value == nonAuthorityComponent.NetworkVariableData4.Value, $"[{authorityComponent.name}:{netVarName4}] Server side instance value ({authorityComponent.NetworkVariableData4.Value}) " + + $"does not match the client side instance value ({nonAuthorityComponent.NetworkVariableData4.Value})!"); } /// @@ -322,7 +279,8 @@ private bool ClientSpawnedNetworkObjects(List spawnedObjectList) [UnityTest] public IEnumerator NetworkBehaviourSynchronization() { - m_ServerNetworkManager.LogLevel = LogLevel.Normal; + var authority = GetAuthorityNetworkManager(); + authority.LogLevel = LogLevel.Normal; m_CurrentLogLevel = LogLevel.Normal; NetworkBehaviourSynchronizeFailureComponent.ResetBehaviour(); @@ -331,28 +289,29 @@ public IEnumerator NetworkBehaviourSynchronization() // Spawn 11 more NetworkObjects where there should be 4 of each failure type for (int i = 0; i < numberOfObjectsToSpawn; i++) { - var synchronizationObject = SpawnObject(m_SynchronizationPrefab, m_ServerNetworkManager); + var synchronizationObject = SpawnObject(m_SynchronizationPrefab, authority); var synchronizationBehaviour = synchronizationObject.GetComponent(); synchronizationBehaviour.AssignNextFailureType(); spawnedObjectList.Add(synchronizationObject); } // Now spawn and connect a client that will fail to spawn half of the NetworkObjects spawned - yield return CreateAndStartNewClient(); + var newClient = CreateNewClient(); + yield return StartClient(newClient); // Validate that when a NetworkBehaviour fails to synchronize and is skipped over it does not // impact the rest of the NetworkBehaviours. - var clientSideNetworkObjects = s_GlobalNetworkObjects[m_ClientNetworkManagers[0].LocalClientId]; - yield return WaitForConditionOrTimeOut(() => ClientSpawnedNetworkObjects(spawnedObjectList)); + var clientSideNetworkObjects = s_GlobalNetworkObjects[newClient.LocalClientId]; + yield return WaitForSpawnedOnAllOrTimeOut(clientSideNetworkObjects.Values); AssertOnTimeout($"Timed out waiting for newly joined client to spawn all NetworkObjects!"); foreach (var spawnedObject in spawnedObjectList) { - var serverSideSpawnedNetworkObject = spawnedObject.GetComponent(); - var clientSideObject = clientSideNetworkObjects[serverSideSpawnedNetworkObject.NetworkObjectId]; - var clientSideSpawnedNetworkObject = clientSideObject.GetComponent(); + var authorityObject = spawnedObject.GetComponent(); + var nonAuthorityObject = clientSideNetworkObjects[authorityObject.NetworkObjectId]; + var clientSideSpawnedNetworkObject = nonAuthorityObject.GetComponent(); - ValidateNetworkBehaviourWithNetworkVariables(serverSideSpawnedNetworkObject, clientSideSpawnedNetworkObject); + ValidateNetworkBehaviourWithNetworkVariables(authorityObject, clientSideSpawnedNetworkObject); } } @@ -362,12 +321,14 @@ public IEnumerator NetworkBehaviourSynchronization() [UnityTest] public IEnumerator NetworkBehaviourOnSynchronize() { - var serverSideInstance = SpawnObject(m_OnSynchronizePrefab, m_ServerNetworkManager).GetComponent(); + var authority = GetAuthorityNetworkManager(); + var serverSideInstance = SpawnObject(m_OnSynchronizePrefab, authority).GetComponent(); // Now spawn and connect a client that will have custom serialized data applied during the client synchronization process. - yield return CreateAndStartNewClient(); + var newClient = CreateNewClient(); + yield return StartClient(newClient); - var clientSideNetworkObjects = s_GlobalNetworkObjects[m_ClientNetworkManagers[0].LocalClientId]; + var clientSideNetworkObjects = s_GlobalNetworkObjects[newClient.LocalClientId]; var clientSideInstance = clientSideNetworkObjects[serverSideInstance.NetworkObjectId].GetComponent(); // Validate the values match @@ -386,13 +347,13 @@ public IEnumerator NetworkBehaviourOnSynchronize() /// internal class NetworkBehaviourWithNetworkVariables : NetworkBehaviour { - public static int ServerSpawnCount { get; internal set; } - public static readonly Dictionary ClientSpawnCount = new Dictionary(); + public static int AuthoritySpawnCount { get; internal set; } + public static readonly Dictionary NonAuthoritySpawnCount = new Dictionary(); public static void ResetSpawnCount() { - ServerSpawnCount = 0; - ClientSpawnCount.Clear(); + AuthoritySpawnCount = 0; + NonAuthoritySpawnCount.Clear(); } private const uint k_MinDataBlocks = 1; @@ -424,15 +385,15 @@ public override void OnNetworkSpawn() { if (IsServer) { - ServerSpawnCount++; + AuthoritySpawnCount++; } else { - if (!ClientSpawnCount.ContainsKey(NetworkManager.LocalClientId)) + if (!NonAuthoritySpawnCount.ContainsKey(NetworkManager.LocalClientId)) { - ClientSpawnCount.Add(NetworkManager.LocalClientId, 0); + NonAuthoritySpawnCount.Add(NetworkManager.LocalClientId, 0); } - ClientSpawnCount[NetworkManager.LocalClientId]++; + NonAuthoritySpawnCount[NetworkManager.LocalClientId]++; } base.OnNetworkSpawn(); @@ -496,8 +457,8 @@ public override void OnNetworkSpawn() internal class NetworkBehaviourSynchronizeFailureComponent : NetworkBehaviour { public static int NumberOfFailureTypes { get; internal set; } - public static int ServerSpawnCount { get; internal set; } - public static int ClientSpawnCount { get; internal set; } + public static int AuthoritySpawnCount { get; internal set; } + public static int NonAuthoritySpawnCount { get; internal set; } private static FailureTypes s_FailureType = FailureTypes.None; @@ -513,8 +474,8 @@ public enum FailureTypes public static void ResetBehaviour() { - ServerSpawnCount = 0; - ClientSpawnCount = 0; + AuthoritySpawnCount = 0; + NonAuthoritySpawnCount = 0; s_FailureType = FailureTypes.None; NumberOfFailureTypes = System.Enum.GetValues(typeof(FailureTypes)).Length; } @@ -595,6 +556,7 @@ public void NetworkSerialize(BufferSerializer serializer) where T : IReade } case FailureTypes.DontReadAnything: { + Debug.Log("Don't read anything is being run"); // Don't read anything break; } @@ -641,14 +603,14 @@ private void Awake() public override void OnNetworkSpawn() { - if (IsServer) + if (HasAuthority) { - ServerSpawnCount++; + AuthoritySpawnCount++; m_MyCustomData.GenerateData((ushort)Random.Range(1, 512)); } else { - ClientSpawnCount++; + NonAuthoritySpawnCount++; } base.OnNetworkSpawn(); diff --git a/com.unity.netcode.gameobjects/Tests/Runtime/Prefabs/NetworkPrefabHandlerSynchronizationTests.cs b/com.unity.netcode.gameobjects/Tests/Runtime/Prefabs/NetworkPrefabHandlerSynchronizationTests.cs new file mode 100644 index 0000000000..d135f29b91 --- /dev/null +++ b/com.unity.netcode.gameobjects/Tests/Runtime/Prefabs/NetworkPrefabHandlerSynchronizationTests.cs @@ -0,0 +1,88 @@ +using System.Collections; +using NUnit.Framework; +using Unity.Netcode.TestHelpers.Runtime; +using UnityEngine; +using UnityEngine.TestTools; + +namespace Unity.Netcode.RuntimeTests +{ + [TestFixture(HostOrServer.Host)] + [TestFixture(HostOrServer.DAHost)] + internal class NetworkPrefabHandlerSynchronizationTests : NetcodeIntegrationTest + { + protected override int NumberOfClients => 1; + + public NetworkPrefabHandlerSynchronizationTests(HostOrServer hostOrServer) : base(hostOrServer) { } + + private GameObject m_ValidPrefab; + private GameObject m_ClientSideValidPrefab; + private GameObject m_ClientSideExceptionPrefab; + + protected override void OnServerAndClientsCreated() + { + m_ValidPrefab = CreateNetworkObjectPrefab("ValidPrefab"); + m_ClientSideValidPrefab = CreateNetworkObjectPrefab("ClientSideValidPrefab"); + m_ClientSideExceptionPrefab = CreateNetworkObjectPrefab("ClientSideExceptionPrefab"); + base.OnServerAndClientsCreated(); + } + + [UnityTest] + public IEnumerator NetworkPrefabHandlerSpawnAndSynchronizeTests() + { + var nonAuthority = GetNonAuthorityNetworkManager(); + + var networkObjectToSpawnOnClient = m_ClientSideValidPrefab.GetComponent(); + nonAuthority.PrefabHandler.AddHandler(m_ClientSideExceptionPrefab, new NetworkPrefabExceptionThrower()); + nonAuthority.PrefabHandler.AddHandler(m_ValidPrefab, new NetworkPrefabInstanceHandler(networkObjectToSpawnOnClient)); + + var authority = GetAuthorityNetworkManager(); + + // Spawn the invalid object first. + var exceptionObject = SpawnObject(m_ClientSideExceptionPrefab, authority).GetComponent(); + + // Check the invalid object spawns on the authority, expect an error from non-authority. + LogAssert.Expect(LogType.Exception, "Exception: exception while instantiating"); + LogAssert.Expect(LogType.Error, $"[Netcode] [GlobalObjectIdHash={exceptionObject.GlobalObjectIdHash}] Failed to spawn NetworkObject!"); + yield return WaitForConditionOrTimeOut(() => exceptionObject.IsSpawned); + AssertOnTimeout("Failed to spawn object on authority!"); + + // Now spawn a valid object + var validObject = SpawnObject(m_ValidPrefab, authority).GetComponent(); + + // The valid object should spawn as expected + yield return WaitForSpawnedOnAllOrTimeOut(validObject); + AssertOnTimeout("Failed to spawn valid prefab on all clients!"); + + // Create a new client and register the same PrefabHandlers on the client + var newClient = CreateNewClient(); + newClient.PrefabHandler.AddHandler(m_ClientSideExceptionPrefab, new NetworkPrefabExceptionThrower()); + newClient.PrefabHandler.AddHandler(m_ValidPrefab, new NetworkPrefabInstanceHandler(networkObjectToSpawnOnClient)); + + // Expect assertions fromt the new client + LogAssert.Expect(LogType.Exception, "Exception: exception while instantiating"); + LogAssert.Expect(LogType.Error, $"[Netcode] [GlobalObjectIdHash={exceptionObject.GlobalObjectIdHash}] Failed to spawn NetworkObject!"); + + // Start and synchronize the new client + yield return StartClient(newClient); + + // Validate the valid prefab spawned on all clients without issue + var expectedAuthorityHash = m_ValidPrefab.GetComponent().GlobalObjectIdHash; + var expectedNonAuthorityHash = m_ClientSideValidPrefab.GetComponent().GlobalObjectIdHash; + foreach (var networkManager in m_NetworkManagers) + { + Assert.True(networkManager.SpawnManager.SpawnedObjects.TryGetValue(validObject.NetworkObjectId, out NetworkObject spawnedObject), $"Client-{networkManager.LocalClientId} failed to spawn version of valid object!"); + + if (spawnedObject.HasAuthority) + { + Assert.That(spawnedObject.GlobalObjectIdHash, Is.EqualTo(expectedAuthorityHash), "NetworkObject spawned with unexpected GlobalObjectIdHash!"); + Assert.That(networkManager.SpawnManager.SpawnedObjects.ContainsKey(exceptionObject.NetworkObjectId), Is.True, "Authority missing spawned NetworkObject!"); + } + else + { + Assert.That(spawnedObject.GlobalObjectIdHash, Is.EqualTo(expectedNonAuthorityHash), "NetworkObject spawned with unexpected GlobalObjectIdHash!"); + Assert.That(networkManager.SpawnManager.SpawnedObjects.ContainsKey(exceptionObject.NetworkObjectId), Is.False, "Non authority should not have spawned exception object!"); + } + } + } + } +} diff --git a/com.unity.netcode.gameobjects/Tests/Runtime/Prefabs/NetworkPrefabHandlerSynchronizationTests.cs.meta b/com.unity.netcode.gameobjects/Tests/Runtime/Prefabs/NetworkPrefabHandlerSynchronizationTests.cs.meta new file mode 100644 index 0000000000..a0c417d419 --- /dev/null +++ b/com.unity.netcode.gameobjects/Tests/Runtime/Prefabs/NetworkPrefabHandlerSynchronizationTests.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 63f7e166459749bfbbf88e5b82ab917d +timeCreated: 1783026731 \ No newline at end of file diff --git a/com.unity.netcode.gameobjects/Tests/Runtime/TestHelpers/NetcodeIntegrationTest.cs b/com.unity.netcode.gameobjects/Tests/Runtime/TestHelpers/NetcodeIntegrationTest.cs index f680b24338..11c092a8a9 100644 --- a/com.unity.netcode.gameobjects/Tests/Runtime/TestHelpers/NetcodeIntegrationTest.cs +++ b/com.unity.netcode.gameobjects/Tests/Runtime/TestHelpers/NetcodeIntegrationTest.cs @@ -1987,7 +1987,7 @@ protected IEnumerator WaitForSpawnedOnAllOrTimeOut(GameObject gameObject, Timeou /// The list of s to wait for. /// An optional to control the timeout period. If null, the default timeout is used. /// An for use in Unity coroutines. - protected IEnumerator WaitForSpawnedOnAllOrTimeOut(List networkObjects, TimeoutHelper timeOutHelper = null) + protected IEnumerator WaitForSpawnedOnAllOrTimeOut(ICollection networkObjects, TimeoutHelper timeOutHelper = null) { bool ValidateObjectsSpawnedOnAllClients(StringBuilder errorLog) { From f4dfd4e117cb24328316b20ccf31ca9c89afeea0 Mon Sep 17 00:00:00 2001 From: Nikos Date: Wed, 8 Jul 2026 23:49:34 +0100 Subject: [PATCH 24/26] ci: Fix code coverage command in YAML configuration (#4069) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fix code coverage command in YAML configuration Coverage reporting was not setup due to this * Moving CodeCov check to daily trigger --------- Co-authored-by: MichaƂ Chrobot --- .yamato/_triggers.yml | 4 ++-- .yamato/code-coverage.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.yamato/_triggers.yml b/.yamato/_triggers.yml index 8cdf8445ce..cf304709d7 100644 --- a/.yamato/_triggers.yml +++ b/.yamato/_triggers.yml @@ -157,6 +157,8 @@ develop_nightly: - .yamato/project-updated-dependencies-test.yml#updated-dependencies_testproject_NGO_win_6000.0 # Run API validation to early-detect all new APIs that would force us to release new minor version of the package. Note that for this to work the package version in package.json must correspond to "actual package state" which means that it should be higher than last released version - .yamato/vetting-test.yml#vetting_test + # Run code coverage test + - .yamato/code-coverage.yml#code_coverage_ubuntu_{{ validation_editors.default }} # Run all tests on weekly bases @@ -187,5 +189,3 @@ develop_weekly_trunk: - .yamato/_run-all.yml#run_all_webgl_builds # Run Runtime tests against CMB service - .yamato/_run-all.yml#run_all_project_tests_cmb_service - # Run code coverage test - - .yamato/code-coverage.yml#code_coverage_ubuntu_{{ validation_editors.default }} diff --git a/.yamato/code-coverage.yml b/.yamato/code-coverage.yml index caeeff72ec..64a2692225 100644 --- a/.yamato/code-coverage.yml +++ b/.yamato/code-coverage.yml @@ -39,7 +39,7 @@ code_coverage_{{ platform.name }}_{{ editor }}: commands: - unity-downloader-cli --fast --wait -u {{ editor }} -c Editor {% if platform.name == "mac" %} --arch arm64 {% endif %} # For macOS we use ARM64 models - upm-pvp create-test-project test-project --packages "upm-ci~/packages/*.tgz" --unity .Editor - - UnifiedTestRunner --suite=editor --suite=playmode --editor-location=.Editor --testproject=test-project --enable-code-coverage coverage-upload-options="reportsDir:$PWD/test-results/CoverageResults;name:NGOv2_{{ platform.name }}_{{ editor }};flags:NGOv2_{{ platform.name }}_{{ editor }};verbose" --coverage-results-path=$PWD/test-results/CoverageResults --coverage-options="generateHtmlReport;generateAdditionalMetrics;assemblyFilters:+Unity.Netcode.Editor,+Unity.Netcode.Runtime" --extra-editor-arg=--burst-disable-compilation --timeout={{ test_timeout }} --rerun-strategy=Test --retry={{ num_test_retries }} --clean-library-on-rerun --artifacts-path=test-results + - UnifiedTestRunner --suite=editor --suite=playmode --editor-location=.Editor --testproject=test-project --enable-code-coverage --coverage-upload-options="reportsDir:$PWD/test-results/CoverageResults;name:NGOv2_{{ platform.name }}_{{ editor }};flags:NGOv2_{{ platform.name }}_{{ editor }};verbose" --coverage-results-path=$PWD/test-results/CoverageResults --coverage-options="generateHtmlReport;generateAdditionalMetrics;assemblyFilters:+Unity.Netcode.Editor,+Unity.Netcode.Runtime" --extra-editor-arg=--burst-disable-compilation --timeout={{ test_timeout }} --rerun-strategy=Test --retry={{ num_test_retries }} --clean-library-on-rerun --artifacts-path=test-results artifacts: logs: paths: From 3e579edf3bfd0b970afbe5e6b6d96370a507e20a Mon Sep 17 00:00:00 2001 From: Noel Stephens Date: Sat, 11 Jul 2026 12:29:52 -0500 Subject: [PATCH 25/26] fix: soft synchronization errors if loading a new scene and setting it as active (#4065) * fix - NetworkObject null exception on warning log If the NetworkObject was not found then use "null" as the name of the NetworkObject. * fix - synchronizing clients should always load active scene first This fixes the issue where the active scene could end up not being the 1st scene loaded which upon loading the active scene and previously SceneEventData relative loaded scenes would get unloaded when loading the active scene since this will can end up being loaded in SingleMode. * fix - prevent marking dynamically spawned objects as in-scene This fix just ignores any spawned objects when setting any just loaded InScenePlaced objects while running in the editor. * style finishing an incomplete comment * update Removing the changes as this will be fixed in a separate PR. * update Change log entry. --- com.unity.netcode.gameobjects/CHANGELOG.md | 1 + .../SceneManagement/NetworkSceneManager.cs | 90 ++++++++++--------- 2 files changed, 51 insertions(+), 40 deletions(-) diff --git a/com.unity.netcode.gameobjects/CHANGELOG.md b/com.unity.netcode.gameobjects/CHANGELOG.md index 5374068ba0..f8ca65e0ab 100644 --- a/com.unity.netcode.gameobjects/CHANGELOG.md +++ b/com.unity.netcode.gameobjects/CHANGELOG.md @@ -23,6 +23,7 @@ Additional documentation and release notes are available at [Multiplayer Documen ### Fixed - Issue where a NullReferenceException was thrown when a non-authority failed to spawn a NetworkObject. (#4067) +- Issue where the active scene was not being serialized as the 1st scene which could result in various errors including a soft synchronization error if, on the client-side, other synchronized scenes had already been loaded prior to the active scene, which is always loaded as `LoadSceneMode.SingleMode` when client synchronization is set to `LoadSceneMode.SingleMode`, resulting in the previously loaded scene(s) to be unloaded. (#4065) - Issue when FastBufferReader is attempting to read a string and all or a portion of the character count has already been read by user script, it could read a character length that results in a negative byte length which could result in an editor crash. (#4052) - Issue where NetworkRigidbodyBase was not applying rotation correctly when using Rigidbody2D. (#4012) - Issue where NetworkRigidbodyBase was always checking the 3D rigid body's interpolation mode when determining if it is kinematic and needs to put the rigid body to sleep and then switch to interpolation. (#4012) diff --git a/com.unity.netcode.gameobjects/Runtime/SceneManagement/NetworkSceneManager.cs b/com.unity.netcode.gameobjects/Runtime/SceneManagement/NetworkSceneManager.cs index a28baf1f94..c0d4b44aee 100644 --- a/com.unity.netcode.gameobjects/Runtime/SceneManagement/NetworkSceneManager.cs +++ b/com.unity.netcode.gameobjects/Runtime/SceneManagement/NetworkSceneManager.cs @@ -1951,6 +1951,21 @@ private void OnClientLoadedScene(uint sceneEventId, Scene scene) /// internal List ClientConnectionQueue = new List(); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void AddSceneToClientSynchronization(ref SceneEventData sceneEventData, ref Scene scene) + { + // If we are just a normal client and in distributed authority mode, then always use the known server scene handle + if (NetworkManager.DistributedAuthorityMode && NetworkManager.CMBServiceConnection) + { + sceneEventData.AddSceneToSynchronize(SceneHashFromNameOrPath(scene.path), ClientSceneHandleToServerSceneHandle[scene.handle]); + } + else + { + sceneEventData.AddSceneToSynchronize(SceneHashFromNameOrPath(scene.path), scene.handle); + } + } + /// /// Server Side: /// This is used for players that have just had their connection approved and will assure they are synchronized @@ -2001,61 +2016,53 @@ internal void SynchronizeNetworkObjects(ulong clientId, bool synchronizingServic // Organize how (and when) we serialize our NetworkObjects var hasSynchronizedActive = false; - for (int i = 0; i < SceneManager.sceneCount; i++) - { - var scene = SceneManager.GetSceneAt(i); - // NetworkSceneManager does not synchronize scenes that are not loaded by NetworkSceneManager - // unless the scene in question is the currently active scene. - if (ExcludeSceneFromSychronization != null && !ExcludeSceneFromSychronization(scene)) + // It is possible a user might not want to synchronize the active scene, so we will check to see if it is valid before adding it to the synchronization list. + // !! Important !! + // The active scene MUST always be the first scene in the synchronization list. + if (ValidateSceneBeforeLoading(activeScene.buildIndex, activeScene.name, sceneEventData.LoadSceneMode)) + { + sceneEventData.SceneHash = SceneHashFromNameOrPath(activeScene.path); + if (sceneEventData.SceneHash == sceneEventData.ActiveSceneHash) { - continue; + hasSynchronizedActive = true; } - if (scene == DontDestroyOnLoadScene) + // If we are just a normal client, then always use the server scene handle + if (NetworkManager.DistributedAuthorityMode) { - continue; + sceneEventData.SenderClientId = NetworkManager.LocalClientId; + sceneEventData.SceneHandle = ClientSceneHandleToServerSceneHandle[activeScene.handle]; } - - // This would depend upon whether we are additive or not - // If we are the base scene, then we set the root scene index; - if (activeScene == scene) + else { - if (!ValidateSceneBeforeLoading(scene.buildIndex, scene.name, sceneEventData.LoadSceneMode)) - { - continue; - } - sceneEventData.SceneHash = SceneHashFromNameOrPath(scene.path); - if (sceneEventData.SceneHash == sceneEventData.ActiveSceneHash) - { - hasSynchronizedActive = true; - } - - // If we are just a normal client, then always use the server scene handle - if (NetworkManager.DistributedAuthorityMode) - { - sceneEventData.SenderClientId = NetworkManager.LocalClientId; - sceneEventData.SceneHandle = ClientSceneHandleToServerSceneHandle[scene.handle]; - } - else - { - sceneEventData.SceneHandle = scene.handle; - } + sceneEventData.SceneHandle = activeScene.handle; } - else if (!ValidateSceneBeforeLoading(scene.buildIndex, scene.name, LoadSceneMode.Additive)) + AddSceneToClientSynchronization(ref sceneEventData, ref activeScene); + } + + for (int i = 0; i < SceneManager.sceneCount; i++) + { + var scene = SceneManager.GetSceneAt(i); + // Skip adding the active scene at this point as we are just adding all other additively loaded scenes to the synchronization list. + // Skip adding the dont destroy on load scene as that is never synchronized. + if ((scene.handle == activeScene.handle) || (scene == DontDestroyOnLoadScene)) { continue; } - // If we are just a normal client and in distributed authority mode, then always use the known server scene handle - if (NetworkManager.DistributedAuthorityMode && NetworkManager.CMBServiceConnection) + // NetworkSceneManager does not synchronize scenes that are not loaded by NetworkSceneManager + // unless the scene in question is the currently active scene. + if (ExcludeSceneFromSychronization != null && !ExcludeSceneFromSychronization(scene)) { - sceneEventData.AddSceneToSynchronize(SceneHashFromNameOrPath(scene.path), ClientSceneHandleToServerSceneHandle[scene.handle]); + continue; } - else + + if (!ValidateSceneBeforeLoading(scene.buildIndex, scene.name, LoadSceneMode.Additive)) { - sceneEventData.AddSceneToSynchronize(SceneHashFromNameOrPath(scene.path), scene.handle); + continue; } + AddSceneToClientSynchronization(ref sceneEventData, ref scene); } if (!hasSynchronizedActive && NetworkManager.CMBServiceConnection && synchronizingService) @@ -2108,9 +2115,12 @@ private void OnClientBeginSync(uint sceneEventId) var sceneHash = sceneEventData.GetNextSceneSynchronizationHash(); var sceneHandle = sceneEventData.GetNextSceneSynchronizationHandle(); var sceneName = SceneNameFromHash(sceneHash); + var activeSceneName = SceneNameFromHash(sceneEventData.ActiveSceneHash); var activeScene = SceneManager.GetActiveScene(); - var loadSceneMode = sceneHash == sceneEventData.SceneHash ? sceneEventData.LoadSceneMode : LoadSceneMode.Additive; + var activeSceneLoaded = activeSceneName == activeScene.name; + + var loadSceneMode = sceneHash == sceneEventData.SceneHash && !activeSceneLoaded ? sceneEventData.LoadSceneMode : LoadSceneMode.Additive; // Store the sceneHandle and hash sceneEventData.NetworkSceneHandle = sceneHandle; From 2aa38fe9a52dbf533859df9b287820259b8c1c34 Mon Sep 17 00:00:00 2001 From: Noel Stephens Date: Sun, 12 Jul 2026 16:38:34 -0500 Subject: [PATCH 26/26] fix Fixing some minor merge artifacts. --- .../Runtime/SceneManagement/SceneEventData.cs | 6 +++--- .../Runtime/Serialization/FastBufferReader.cs | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/com.unity.netcode.gameobjects/Runtime/SceneManagement/SceneEventData.cs b/com.unity.netcode.gameobjects/Runtime/SceneManagement/SceneEventData.cs index 1ad1221eda..0b65c08ed9 100644 --- a/com.unity.netcode.gameobjects/Runtime/SceneManagement/SceneEventData.cs +++ b/com.unity.netcode.gameobjects/Runtime/SceneManagement/SceneEventData.cs @@ -1104,7 +1104,7 @@ internal void SynchronizeSceneNetworkObjects(NetworkManager networkManager) // information during synchronization. if (serializedObject.HasGhost) { - if (networkManager.SpawnManager.GhostSpawnManager.ShouldDeferGhostSceneObject(serializedObject, InternalBuffer)) + if (networkManager.SpawnManager.GhostSpawnManager.ShouldDeferGhostSceneObject(serializedObject, m_InternalBuffer)) { continue; } @@ -1122,10 +1122,10 @@ internal void SynchronizeSceneNetworkObjects(NetworkManager networkManager) continue; } - var noStop = m_InternalBuffer.Position; + if (EnableSerializationLogs) { - var noStop = InternalBuffer.Position; + var noStop = m_InternalBuffer.Position; builder.AppendLine($"[Head: {noStart}][Tail: {noStop}][Size: {noStop - noStart}][{spawnedNetworkObject.name}][NID-{spawnedNetworkObject.NetworkObjectId}][Children: {spawnedNetworkObject.ChildNetworkBehaviours.Count}]"); LogArray(m_InternalBuffer.ToArray(), noStart, noStop, builder); } diff --git a/com.unity.netcode.gameobjects/Runtime/Serialization/FastBufferReader.cs b/com.unity.netcode.gameobjects/Runtime/Serialization/FastBufferReader.cs index 66460449b5..99826f39e3 100644 --- a/com.unity.netcode.gameobjects/Runtime/Serialization/FastBufferReader.cs +++ b/com.unity.netcode.gameobjects/Runtime/Serialization/FastBufferReader.cs @@ -633,7 +633,7 @@ public unsafe void ReadValue(out string s, bool oneByteChars = false) } #endif - if (!TryBeginReadInternal(SizeOfLengthField())) + if (!TryBeginReadInternal(sizeof(int))) { throw new OverflowException("Reading past the end of the buffer"); }