diff --git a/com.unity.netcode.gameobjects/CHANGELOG.md b/com.unity.netcode.gameobjects/CHANGELOG.md index 379f428b98..0a1ac3a860 100644 --- a/com.unity.netcode.gameobjects/CHANGELOG.md +++ b/com.unity.netcode.gameobjects/CHANGELOG.md @@ -5,6 +5,15 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). Additional documentation and release notes are available at [Multiplayer Documentation](https://docs-multiplayer.unity3d.com). +## [Unreleased] + +### Added + +### Fixed + +- Fixed issue where an in-scene placed `NetworkObject` with `NetworkTransform` that is also parented under a `GameObject` would not properly synchronize when the parent `GameObject` had a world space position other than 0,0,0. (#2895) + +### Changed ## [Unreleased] diff --git a/com.unity.netcode.gameobjects/Components/NetworkTransform.cs b/com.unity.netcode.gameobjects/Components/NetworkTransform.cs index 96d3f72a8d..13f17e99e5 100644 --- a/com.unity.netcode.gameobjects/Components/NetworkTransform.cs +++ b/com.unity.netcode.gameobjects/Components/NetworkTransform.cs @@ -1682,6 +1682,15 @@ private bool ApplyTransformToNetworkStateWithInfo(ref NetworkTransformState netw var scale = transformToUse.localScale; networkState.IsSynchronizing = isSynchronization; + // All of the checks below, up to the delta position checking portion, are to determine if the + // authority changed a property during runtime that requires a full synchronizing. + if (InLocalSpace != networkState.InLocalSpace) + { + networkState.InLocalSpace = InLocalSpace; + isDirty = true; + networkState.IsTeleportingNextFrame = true; + } + // Check for parenting when synchronizing and/or teleporting if (isSynchronization || networkState.IsTeleportingNextFrame) { @@ -1691,11 +1700,13 @@ private bool ApplyTransformToNetworkStateWithInfo(ref NetworkTransformState netw // 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) { - var parentNetworkObject = NetworkObject.transform.parent.GetComponent(); + parentNetworkObject = NetworkObject.transform.parent.GetComponent(); // In-scene placed NetworkObjects parented under a GameObject with no // NetworkObject preserve their lossyScale when synchronizing. @@ -1716,28 +1727,25 @@ private bool ApplyTransformToNetworkStateWithInfo(ref NetworkTransformState netw // the NetworkTransform is using world or local space synchronization. // WorldPositionStays: (always use world space) // !WorldPositionStays: (always use local space) - if (isSynchronization) + // Exception: If it is an in-scene placed NetworkObject and it is parented under a GameObject + // then always use local space unless AutoObjectParentSync is disabled and the NetworkTransform + // is synchronizing in world space. + if (isSynchronization && networkState.IsParented) { - if (NetworkObject.WorldPositionStays()) + var parentedUnderGameObject = NetworkObject.transform.parent != null && !parentNetworkObject && NetworkObject.IsSceneObject.Value; + if (NetworkObject.WorldPositionStays() && (!parentedUnderGameObject || (parentedUnderGameObject && !NetworkObject.AutoObjectParentSync && !InLocalSpace))) { position = transformToUse.position; + networkState.InLocalSpace = false; } else { position = transformToUse.localPosition; + networkState.InLocalSpace = true; } } } - // All of the checks below, up to the delta position checking portion, are to determine if the - // authority changed a property during runtime that requires a full synchronizing. - if (InLocalSpace != networkState.InLocalSpace) - { - networkState.InLocalSpace = InLocalSpace; - isDirty = true; - networkState.IsTeleportingNextFrame = true; - } - if (Interpolate != networkState.UseInterpolation) { networkState.UseInterpolation = Interpolate; diff --git a/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkSpawnManager.cs b/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkSpawnManager.cs index ecaf6a41b5..794f08d21b 100644 --- a/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkSpawnManager.cs +++ b/com.unity.netcode.gameobjects/Runtime/Spawning/NetworkSpawnManager.cs @@ -531,20 +531,27 @@ internal NetworkObject CreateLocalNetworkObject(NetworkObject.SceneObject sceneO networkObject.DestroyWithScene = sceneObject.DestroyWithScene; networkObject.NetworkSceneHandle = sceneObject.NetworkSceneHandle; + + var nonNetworkObjectParent = false; // SPECIAL CASE FOR IN-SCENE PLACED: (only when the parent has a NetworkObject) // This is a special case scenario where a late joining client has joined and loaded one or // more scenes that contain nested in-scene placed NetworkObject children yet the server's // synchronization information does not indicate the NetworkObject in question has a parent. // Under this scenario, we want to remove the parent before spawning and setting the transform values. - if (sceneObject.IsSceneObject && !sceneObject.HasParent && networkObject.transform.parent != null) + if (sceneObject.IsSceneObject && networkObject.transform.parent != null) { + var parentNetworkObject = networkObject.transform.parent.GetComponent(); // if the in-scene placed NetworkObject has a parent NetworkObject but the synchronization information does not // include parenting, then we need to force the removal of that parent - if (networkObject.transform.parent.GetComponent() != null) + if (!sceneObject.HasParent && parentNetworkObject) { // remove the parent networkObject.ApplyNetworkParenting(true, true); } + else if (sceneObject.HasParent && !parentNetworkObject) + { + nonNetworkObjectParent = true; + } } // Set the transform unless we were spawned by a prefab handler @@ -554,7 +561,7 @@ internal NetworkObject CreateLocalNetworkObject(NetworkObject.SceneObject sceneO { // If world position stays is true or we have auto object parent synchronization disabled // then we want to apply the position and rotation values world space relative - if (worldPositionStays || !networkObject.AutoObjectParentSync) + if ((worldPositionStays && !nonNetworkObjectParent) || !networkObject.AutoObjectParentSync) { networkObject.transform.position = position; networkObject.transform.rotation = rotation; diff --git a/testproject/Assets/Tests/Manual/IntegrationTestScenes/InSceneUnderGameObject.unity b/testproject/Assets/Tests/Manual/IntegrationTestScenes/InSceneUnderGameObject.unity new file mode 100644 index 0000000000..ba9f95cfd3 --- /dev/null +++ b/testproject/Assets/Tests/Manual/IntegrationTestScenes/InSceneUnderGameObject.unity @@ -0,0 +1,229 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!29 &1 +OcclusionCullingSettings: + m_ObjectHideFlags: 0 + serializedVersion: 2 + m_OcclusionBakeSettings: + smallestOccluder: 5 + smallestHole: 0.25 + backfaceThreshold: 100 + m_SceneGUID: 00000000000000000000000000000000 + m_OcclusionCullingData: {fileID: 0} +--- !u!104 &2 +RenderSettings: + m_ObjectHideFlags: 0 + serializedVersion: 9 + m_Fog: 0 + m_FogColor: {r: 0.5, g: 0.5, b: 0.5, a: 1} + m_FogMode: 3 + m_FogDensity: 0.01 + m_LinearFogStart: 0 + m_LinearFogEnd: 300 + m_AmbientSkyColor: {r: 0.212, g: 0.227, b: 0.259, a: 1} + m_AmbientEquatorColor: {r: 0.114, g: 0.125, b: 0.133, a: 1} + m_AmbientGroundColor: {r: 0.047, g: 0.043, b: 0.035, a: 1} + m_AmbientIntensity: 1 + m_AmbientMode: 0 + m_SubtractiveShadowColor: {r: 0.42, g: 0.478, b: 0.627, a: 1} + m_SkyboxMaterial: {fileID: 10304, guid: 0000000000000000f000000000000000, type: 0} + m_HaloStrength: 0.5 + m_FlareStrength: 1 + m_FlareFadeSpeed: 3 + m_HaloTexture: {fileID: 0} + m_SpotCookie: {fileID: 10001, guid: 0000000000000000e000000000000000, type: 0} + m_DefaultReflectionMode: 0 + m_DefaultReflectionResolution: 128 + m_ReflectionBounces: 1 + m_ReflectionIntensity: 1 + m_CustomReflection: {fileID: 0} + m_Sun: {fileID: 0} + m_IndirectSpecularColor: {r: 0.37311953, g: 0.38074014, b: 0.3587274, a: 1} + m_UseRadianceAmbientProbe: 0 +--- !u!157 &3 +LightmapSettings: + m_ObjectHideFlags: 0 + serializedVersion: 12 + m_GIWorkflowMode: 1 + m_GISettings: + serializedVersion: 2 + m_BounceScale: 1 + m_IndirectOutputScale: 1 + m_AlbedoBoost: 1 + m_EnvironmentLightingMode: 0 + m_EnableBakedLightmaps: 1 + m_EnableRealtimeLightmaps: 0 + m_LightmapEditorSettings: + serializedVersion: 12 + m_Resolution: 2 + m_BakeResolution: 40 + m_AtlasSize: 1024 + m_AO: 0 + m_AOMaxDistance: 1 + m_CompAOExponent: 1 + m_CompAOExponentDirect: 0 + m_ExtractAmbientOcclusion: 0 + m_Padding: 2 + m_LightmapParameters: {fileID: 0} + m_LightmapsBakeMode: 1 + m_TextureCompression: 1 + m_FinalGather: 0 + m_FinalGatherFiltering: 1 + m_FinalGatherRayCount: 256 + m_ReflectionCompression: 2 + m_MixedBakeMode: 2 + m_BakeBackend: 1 + m_PVRSampling: 1 + m_PVRDirectSampleCount: 32 + m_PVRSampleCount: 512 + m_PVRBounces: 2 + m_PVREnvironmentSampleCount: 256 + m_PVREnvironmentReferencePointCount: 2048 + m_PVRFilteringMode: 1 + m_PVRDenoiserTypeDirect: 1 + m_PVRDenoiserTypeIndirect: 1 + m_PVRDenoiserTypeAO: 1 + m_PVRFilterTypeDirect: 0 + m_PVRFilterTypeIndirect: 0 + m_PVRFilterTypeAO: 0 + m_PVREnvironmentMIS: 1 + m_PVRCulling: 1 + m_PVRFilteringGaussRadiusDirect: 1 + m_PVRFilteringGaussRadiusIndirect: 5 + m_PVRFilteringGaussRadiusAO: 2 + m_PVRFilteringAtrousPositionSigmaDirect: 0.5 + m_PVRFilteringAtrousPositionSigmaIndirect: 2 + m_PVRFilteringAtrousPositionSigmaAO: 1 + m_ExportTrainingData: 0 + m_TrainingDataDestination: TrainingData + m_LightProbeSampleCountMultiplier: 4 + m_LightingDataAsset: {fileID: 0} + m_LightingSettings: {fileID: 0} +--- !u!196 &4 +NavMeshSettings: + serializedVersion: 2 + m_ObjectHideFlags: 0 + m_BuildSettings: + serializedVersion: 3 + agentTypeID: 0 + agentRadius: 0.5 + agentHeight: 2 + agentSlope: 45 + agentClimb: 0.4 + ledgeDropHeight: 0 + maxJumpAcrossDistance: 0 + minRegionArea: 2 + manualCellSize: 0 + cellSize: 0.16666667 + manualTileSize: 0 + tileSize: 256 + buildHeightMesh: 0 + maxJobWorkers: 0 + preserveTilesOutsideBounds: 0 + debug: + m_Flags: 0 + m_NavMeshData: {fileID: 0} +--- !u!1 &151892557 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 151892558} + m_Layer: 0 + m_Name: ParentNoNT + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &151892558 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 151892557} + serializedVersion: 2 + m_LocalRotation: {x: 0.6465042, y: 0.021078281, z: 0.723563, w: -0.24092445} + m_LocalPosition: {x: 2, y: 4, z: 1} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: + - {fileID: 386611893} + m_Father: {fileID: 0} + m_LocalEulerAnglesHint: {x: -20, y: 80, z: 200} +--- !u!1 &386611892 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 386611893} + - component: {fileID: 386611894} + - component: {fileID: 386611895} + m_Layer: 0 + m_Name: ChildNoNT + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &386611893 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 386611892} + serializedVersion: 2 + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 151892558} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!114 &386611894 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 386611892} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: d5a57f767e5e46a458fc5d3c628d0cbb, type: 3} + m_Name: + m_EditorClassIdentifier: + GlobalObjectIdHash: 4010782011 + InScenePlacedSourceGlobalObjectIdHash: 0 + AlwaysReplicateAsRoot: 0 + SynchronizeTransform: 1 + ActiveSceneSynchronization: 0 + SceneMigrationSynchronization: 1 + SpawnWithObservers: 1 + DontDestroyWithOwner: 0 + AutoObjectParentSync: 1 +--- !u!114 &386611895 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 386611892} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: bd877da52a3b48f4f867ac2c973c0265, type: 3} + m_Name: + m_EditorClassIdentifier: + ObjectWasDisabledUponSpawn: 0 +--- !u!1660057539 &9223372036854775807 +SceneRoots: + m_ObjectHideFlags: 0 + m_Roots: + - {fileID: 151892558} diff --git a/testproject/Assets/Tests/Manual/IntegrationTestScenes/InSceneUnderGameObject.unity.meta b/testproject/Assets/Tests/Manual/IntegrationTestScenes/InSceneUnderGameObject.unity.meta new file mode 100644 index 0000000000..e8c308a091 --- /dev/null +++ b/testproject/Assets/Tests/Manual/IntegrationTestScenes/InSceneUnderGameObject.unity.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 778ea62ad5daee5408d1ec1fce28673f +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/testproject/Assets/Tests/Manual/IntegrationTestScenes/InSceneUnderGameObjectWithNT.unity b/testproject/Assets/Tests/Manual/IntegrationTestScenes/InSceneUnderGameObjectWithNT.unity new file mode 100644 index 0000000000..7d57191d46 --- /dev/null +++ b/testproject/Assets/Tests/Manual/IntegrationTestScenes/InSceneUnderGameObjectWithNT.unity @@ -0,0 +1,261 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!29 &1 +OcclusionCullingSettings: + m_ObjectHideFlags: 0 + serializedVersion: 2 + m_OcclusionBakeSettings: + smallestOccluder: 5 + smallestHole: 0.25 + backfaceThreshold: 100 + m_SceneGUID: 00000000000000000000000000000000 + m_OcclusionCullingData: {fileID: 0} +--- !u!104 &2 +RenderSettings: + m_ObjectHideFlags: 0 + serializedVersion: 9 + m_Fog: 0 + m_FogColor: {r: 0.5, g: 0.5, b: 0.5, a: 1} + m_FogMode: 3 + m_FogDensity: 0.01 + m_LinearFogStart: 0 + m_LinearFogEnd: 300 + m_AmbientSkyColor: {r: 0.212, g: 0.227, b: 0.259, a: 1} + m_AmbientEquatorColor: {r: 0.114, g: 0.125, b: 0.133, a: 1} + m_AmbientGroundColor: {r: 0.047, g: 0.043, b: 0.035, a: 1} + m_AmbientIntensity: 1 + m_AmbientMode: 0 + m_SubtractiveShadowColor: {r: 0.42, g: 0.478, b: 0.627, a: 1} + m_SkyboxMaterial: {fileID: 10304, guid: 0000000000000000f000000000000000, type: 0} + m_HaloStrength: 0.5 + m_FlareStrength: 1 + m_FlareFadeSpeed: 3 + m_HaloTexture: {fileID: 0} + m_SpotCookie: {fileID: 10001, guid: 0000000000000000e000000000000000, type: 0} + m_DefaultReflectionMode: 0 + m_DefaultReflectionResolution: 128 + m_ReflectionBounces: 1 + m_ReflectionIntensity: 1 + m_CustomReflection: {fileID: 0} + m_Sun: {fileID: 0} + m_IndirectSpecularColor: {r: 0, g: 0, b: 0, a: 1} + m_UseRadianceAmbientProbe: 0 +--- !u!157 &3 +LightmapSettings: + m_ObjectHideFlags: 0 + serializedVersion: 12 + m_GIWorkflowMode: 1 + m_GISettings: + serializedVersion: 2 + m_BounceScale: 1 + m_IndirectOutputScale: 1 + m_AlbedoBoost: 1 + m_EnvironmentLightingMode: 0 + m_EnableBakedLightmaps: 1 + m_EnableRealtimeLightmaps: 0 + m_LightmapEditorSettings: + serializedVersion: 12 + m_Resolution: 2 + m_BakeResolution: 40 + m_AtlasSize: 1024 + m_AO: 0 + m_AOMaxDistance: 1 + m_CompAOExponent: 1 + m_CompAOExponentDirect: 0 + m_ExtractAmbientOcclusion: 0 + m_Padding: 2 + m_LightmapParameters: {fileID: 0} + m_LightmapsBakeMode: 1 + m_TextureCompression: 1 + m_FinalGather: 0 + m_FinalGatherFiltering: 1 + m_FinalGatherRayCount: 256 + m_ReflectionCompression: 2 + m_MixedBakeMode: 2 + m_BakeBackend: 1 + m_PVRSampling: 1 + m_PVRDirectSampleCount: 32 + m_PVRSampleCount: 512 + m_PVRBounces: 2 + m_PVREnvironmentSampleCount: 256 + m_PVREnvironmentReferencePointCount: 2048 + m_PVRFilteringMode: 1 + m_PVRDenoiserTypeDirect: 1 + m_PVRDenoiserTypeIndirect: 1 + m_PVRDenoiserTypeAO: 1 + m_PVRFilterTypeDirect: 0 + m_PVRFilterTypeIndirect: 0 + m_PVRFilterTypeAO: 0 + m_PVREnvironmentMIS: 1 + m_PVRCulling: 1 + m_PVRFilteringGaussRadiusDirect: 1 + m_PVRFilteringGaussRadiusIndirect: 5 + m_PVRFilteringGaussRadiusAO: 2 + m_PVRFilteringAtrousPositionSigmaDirect: 0.5 + m_PVRFilteringAtrousPositionSigmaIndirect: 2 + m_PVRFilteringAtrousPositionSigmaAO: 1 + m_ExportTrainingData: 0 + m_TrainingDataDestination: TrainingData + m_LightProbeSampleCountMultiplier: 4 + m_LightingDataAsset: {fileID: 0} + m_LightingSettings: {fileID: 0} +--- !u!196 &4 +NavMeshSettings: + serializedVersion: 2 + m_ObjectHideFlags: 0 + m_BuildSettings: + serializedVersion: 3 + agentTypeID: 0 + agentRadius: 0.5 + agentHeight: 2 + agentSlope: 45 + agentClimb: 0.4 + ledgeDropHeight: 0 + maxJumpAcrossDistance: 0 + minRegionArea: 2 + manualCellSize: 0 + cellSize: 0.16666667 + manualTileSize: 0 + tileSize: 256 + buildHeightMesh: 0 + maxJobWorkers: 0 + preserveTilesOutsideBounds: 0 + debug: + m_Flags: 0 + m_NavMeshData: {fileID: 0} +--- !u!1 &1010367930 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1010367931} + - component: {fileID: 1010367932} + - component: {fileID: 1010367933} + - component: {fileID: 1010367934} + m_Layer: 0 + m_Name: ChildNT + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &1010367931 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1010367930} + serializedVersion: 2 + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 1523284014} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!114 &1010367932 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1010367930} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: d5a57f767e5e46a458fc5d3c628d0cbb, type: 3} + m_Name: + m_EditorClassIdentifier: + GlobalObjectIdHash: 1318288622 + InScenePlacedSourceGlobalObjectIdHash: 0 + AlwaysReplicateAsRoot: 0 + SynchronizeTransform: 1 + ActiveSceneSynchronization: 0 + SceneMigrationSynchronization: 1 + SpawnWithObservers: 1 + DontDestroyWithOwner: 0 + AutoObjectParentSync: 1 +--- !u!114 &1010367933 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1010367930} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: e96cb6065543e43c4a752faaa1468eb1, type: 3} + m_Name: + m_EditorClassIdentifier: + UseUnreliableDeltas: 0 + SyncPositionX: 1 + SyncPositionY: 1 + SyncPositionZ: 1 + SyncRotAngleX: 1 + SyncRotAngleY: 1 + SyncRotAngleZ: 1 + SyncScaleX: 1 + SyncScaleY: 1 + SyncScaleZ: 1 + PositionThreshold: 0.001 + RotAngleThreshold: 0.01 + ScaleThreshold: 0.01 + UseQuaternionSynchronization: 0 + UseQuaternionCompression: 0 + UseHalfFloatPrecision: 0 + InLocalSpace: 0 + Interpolate: 1 + SlerpPosition: 0 +--- !u!114 &1010367934 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1010367930} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: bd877da52a3b48f4f867ac2c973c0265, type: 3} + m_Name: + m_EditorClassIdentifier: + ObjectWasDisabledUponSpawn: 0 +--- !u!1 &1523284013 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1523284014} + m_Layer: 0 + m_Name: ParentNT + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &1523284014 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1523284013} + serializedVersion: 2 + m_LocalRotation: {x: -0.06162833, y: 0.7044161, z: -0.40557978, w: 0.579228} + m_LocalPosition: {x: 5, y: 2, z: 3} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: + - {fileID: 1010367931} + m_Father: {fileID: 0} + m_LocalEulerAnglesHint: {x: 30, y: 90, z: -40} +--- !u!1660057539 &9223372036854775807 +SceneRoots: + m_ObjectHideFlags: 0 + m_Roots: + - {fileID: 1523284014} diff --git a/testproject/Assets/Tests/Manual/IntegrationTestScenes/InSceneUnderGameObjectWithNT.unity.meta b/testproject/Assets/Tests/Manual/IntegrationTestScenes/InSceneUnderGameObjectWithNT.unity.meta new file mode 100644 index 0000000000..bc6449311e --- /dev/null +++ b/testproject/Assets/Tests/Manual/IntegrationTestScenes/InSceneUnderGameObjectWithNT.unity.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 5a4f489df08d16c4d8c0167b099de2ca +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/testproject/Assets/Tests/Runtime/NetworkSceneManager/InScenePlacedNetworkObjectTests.cs b/testproject/Assets/Tests/Runtime/NetworkSceneManager/InScenePlacedNetworkObjectTests.cs index fa265302f9..09a8d1bf7c 100644 --- a/testproject/Assets/Tests/Runtime/NetworkSceneManager/InScenePlacedNetworkObjectTests.cs +++ b/testproject/Assets/Tests/Runtime/NetworkSceneManager/InScenePlacedNetworkObjectTests.cs @@ -3,6 +3,7 @@ using System.Linq; using NUnit.Framework; using Unity.Netcode; +using Unity.Netcode.Components; using Unity.Netcode.TestHelpers.Runtime; using UnityEngine; using UnityEngine.SceneManagement; @@ -12,21 +13,36 @@ namespace TestProject.RuntimeTests { - public class InScenePlacedNetworkObjectTests : NetcodeIntegrationTest + public class InScenePlacedNetworkObjectTests : IntegrationTestWithApproximation { protected override int NumberOfClients => 2; private const string k_SceneToLoad = "InSceneNetworkObject"; + private const string k_InSceneUnder = "InSceneUnderGameObject"; + private const string k_InSceneUnderWithNT = "InSceneUnderGameObjectWithNT"; private Scene m_ServerSideSceneLoaded; private bool m_CanStartServerAndClients; + private string m_SceneLoading = k_SceneToLoad; protected override IEnumerator OnSetup() { NetworkObjectTestComponent.Reset(); + NetworkObjectTestComponent.VerboseDebug = m_EnableVerboseDebug; m_CanStartServerAndClients = false; return base.OnSetup(); } + /// + /// Very important to always have a backup "unloading" catch + /// in the event your test fails it could not potentially unload + /// a scene and the proceeding tests could be impacted by this! + /// + /// + protected override IEnumerator OnTearDown() + { + yield return CleanUpLoadedScene(); + } + protected override bool CanStartServerAndClients() { return m_CanStartServerAndClients; @@ -183,7 +199,7 @@ public IEnumerator ParentedInSceneObjectLateJoiningClient() private void OnSceneEvent(SceneEvent sceneEvent) { - if (sceneEvent.SceneEventType == SceneEventType.LoadComplete && sceneEvent.SceneName == k_SceneToLoad && sceneEvent.ClientId == m_ClientNetworkManagers[0].LocalClientId) + if (sceneEvent.SceneEventType == SceneEventType.LoadComplete && sceneEvent.SceneName == m_SceneLoading && sceneEvent.ClientId == m_ClientNetworkManagers[0].LocalClientId) { m_ClientLoadedScene = sceneEvent.Scene; } @@ -394,17 +410,168 @@ private void SceneManager_OnLoadEventCompleted(string sceneName, LoadSceneMode l m_SceneLoaded = SceneManager.GetSceneByName(sceneName); } + public enum ParentSyncSettings + { + ParentSync, + NoParentSync + } + + public enum TransformSyncSettings + { + TransformSync, + NoTransformSync + } + public enum TransformSpace + { + World, + Local + } /// - /// Very important to always have a backup "unloading" catch - /// in the event your test fails it could not potentially unload - /// a scene and the proceeding tests could be impacted by this! + /// This test validates the initial synchronization of an in-scene placed NetworkObject parented + /// underneath a GameObject. There are two scenes for this tests where the child NetworkObject does + /// and does not have a NetworkTransform component. /// - /// - protected override IEnumerator OnTearDown() + /// Scene to load + /// settings + /// settings + /// setting (when available) + [UnityTest] + public IEnumerator ParentedInSceneObjectUnderGameObject([Values(k_InSceneUnder, k_InSceneUnderWithNT)] string inSceneUnderToLoad, + [Values] ParentSyncSettings parentSyncSettings, [Values] TransformSyncSettings transformSyncSettings, [Values] TransformSpace transformSpace) { - yield return CleanUpLoadedScene(); + var useNetworkTransform = m_SceneLoading == k_InSceneUnderWithNT; + + m_SceneLoading = inSceneUnderToLoad; + // Because despawning a client will cause it to shutdown and clean everything in the + // scene hierarchy, we have to prevent one of the clients from spawning initially before + // we test synchronizing late joining clients. + // So, we prevent the automatic starting of the server and clients, remove the client we + // will be targeting to join late from the m_ClientNetworkManagers array, start the server + // and the remaining client, despawn the in-scene NetworkObject, and then start and synchronize + // the clientToTest. + var clientToTest = m_ClientNetworkManagers[1]; + var clients = m_ClientNetworkManagers.ToList(); + + // Note: This test is a modified copy of ParentedInSceneObjectLateJoiningClient. + // The 1st client is being ignored in this test and the focus is primarily on the late joining + // 2nd client after adjustments have been made to the child NetworkBehaviour and if applicable + // NetworkTransform. + + clients.Remove(clientToTest); + m_ClientNetworkManagers = clients.ToArray(); + m_CanStartServerAndClients = true; + yield return StartServerAndClients(); + clients.Add(clientToTest); + m_ClientNetworkManagers = clients.ToArray(); + + NetworkObjectTestComponent.ServerNetworkObjectInstance = null; + + m_ClientNetworkManagers[0].SceneManager.OnSceneEvent += OnSceneEvent; + m_ServerNetworkManager.SceneManager.LoadScene(m_SceneLoading, LoadSceneMode.Additive); + yield return WaitForConditionOrTimeOut(() => m_ClientLoadedScene.IsValid() && m_ClientLoadedScene.isLoaded); + AssertOnTimeout($"Timed out waiting for {k_SceneToLoad} scene to be loaded!"); + + m_ClientNetworkManagers[0].SceneManager.OnSceneEvent -= OnSceneEvent; + var serverInSceneObjectInstance = NetworkObjectTestComponent.ServerNetworkObjectInstance; + Assert.IsNotNull(serverInSceneObjectInstance, $"Could not get the server-side registration of {nameof(NetworkObjectTestComponent)}!"); + var firstClientInSceneObjectInstance = NetworkObjectTestComponent.SpawnedInstances.Where((c) => c.NetworkManager == m_ClientNetworkManagers[0]).FirstOrDefault(); + Assert.IsNotNull(firstClientInSceneObjectInstance, $"Could not get the client-side registration of {nameof(NetworkObjectTestComponent)}!"); + Assert.IsTrue(firstClientInSceneObjectInstance.NetworkManager == m_ClientNetworkManagers[0]); + + // Parent the object + var clientSideServerPlayer = m_PlayerNetworkObjects[m_ClientNetworkManagers[0].LocalClientId][NetworkManager.ServerClientId]; + + serverInSceneObjectInstance.AutoObjectParentSync = parentSyncSettings == ParentSyncSettings.ParentSync; + serverInSceneObjectInstance.SynchronizeTransform = transformSyncSettings == TransformSyncSettings.TransformSync; + + var serverNetworkTransform = useNetworkTransform ? serverInSceneObjectInstance.GetComponent() : null; + if (useNetworkTransform) + { + serverNetworkTransform.InLocalSpace = transformSpace == TransformSpace.Local; + } + + // Now late join a client + NetcodeIntegrationTestHelpers.StartOneClient(clientToTest); + yield return WaitForConditionOrTimeOut(() => (clientToTest.IsConnectedClient && clientToTest.IsListening)); + AssertOnTimeout($"Timed out waiting for {clientToTest.name} to reconnect!"); + + yield return s_DefaultWaitForTick; + + // Update the newly joined client information + ClientNetworkManagerPostStartInit(); + + var lateJoinClientInSceneObjectInstance = NetworkObjectTestComponent.SpawnedInstances.Where((c) => c.NetworkManager == m_ClientNetworkManagers[1]).FirstOrDefault(); + Assert.IsNotNull(lateJoinClientInSceneObjectInstance, $"Could not get the client-side registration of {nameof(NetworkObjectTestComponent)} for the late joining client!"); + + // Now make sure the server and newly joined client transform values match. + RotationsMatch(serverInSceneObjectInstance.transform, lateJoinClientInSceneObjectInstance.transform, transformSpace == TransformSpace.Local); + PositionsMatch(serverInSceneObjectInstance.transform, lateJoinClientInSceneObjectInstance.transform, transformSpace == TransformSpace.Local); + // When tesing local space we also do a sanity check and validate the world space values too. + if (transformSpace == TransformSpace.Local) + { + RotationsMatch(serverInSceneObjectInstance.transform, lateJoinClientInSceneObjectInstance.transform); + PositionsMatch(serverInSceneObjectInstance.transform, lateJoinClientInSceneObjectInstance.transform); + } + ScaleValuesMatch(serverInSceneObjectInstance.transform, lateJoinClientInSceneObjectInstance.transform); } + + protected bool RotationsMatch(Transform transformA, Transform transformB, bool inLocalSpace = false) + { + var authorityEulerRotation = inLocalSpace ? transformA.localRotation.eulerAngles : transformA.rotation.eulerAngles; + var nonAuthorityEulerRotation = inLocalSpace ? transformB.localRotation.eulerAngles : transformB.rotation.eulerAngles; + var xIsEqual = ApproximatelyEuler(authorityEulerRotation.x, nonAuthorityEulerRotation.x); + var yIsEqual = ApproximatelyEuler(authorityEulerRotation.y, nonAuthorityEulerRotation.y); + var zIsEqual = ApproximatelyEuler(authorityEulerRotation.z, nonAuthorityEulerRotation.z); + if (!xIsEqual || !yIsEqual || !zIsEqual) + { + VerboseDebug($"[{transformA.gameObject.name}][X-{xIsEqual} | Y-{yIsEqual} | Z-{zIsEqual}]" + + $"Authority rotation {authorityEulerRotation} != [{transformB.gameObject.name}] NonAuthority rotation {nonAuthorityEulerRotation}"); + } + else if (m_EnableVerboseDebug) + { + VerboseDebug($"[{transformA.gameObject.name}][X-{xIsEqual} | Y-{yIsEqual} | Z-{zIsEqual}] " + + $"Authority rotation {authorityEulerRotation} != [{transformB.gameObject.name}] NonAuthority rotation {nonAuthorityEulerRotation}"); + } + return xIsEqual && yIsEqual && zIsEqual; + } + + protected bool PositionsMatch(Transform transformA, Transform transformB, bool inLocalSpace = false) + { + var authorityPosition = inLocalSpace ? transformA.localPosition : transformA.position; + var nonAuthorityPosition = inLocalSpace ? transformB.localPosition : transformB.position; + var xIsEqual = Approximately(authorityPosition.x, nonAuthorityPosition.x); + var yIsEqual = Approximately(authorityPosition.y, nonAuthorityPosition.y); + var zIsEqual = Approximately(authorityPosition.z, nonAuthorityPosition.z); + if (!xIsEqual || !yIsEqual || !zIsEqual) + { + VerboseDebug($"[{transformA.gameObject.name}] Authority position {authorityPosition} != [{transformB.gameObject.name}] NonAuthority position {nonAuthorityPosition}"); + } + else if (m_EnableVerboseDebug) + { + VerboseDebug($"[{transformA.gameObject.name}] Authority position {authorityPosition} != [{transformB.gameObject.name}] NonAuthority position {nonAuthorityPosition}"); + } + return xIsEqual && yIsEqual && zIsEqual; + } + + protected bool ScaleValuesMatch(Transform transformA, Transform transformB) + { + var authorityScale = transformA.localScale; + var nonAuthorityScale = transformB.localScale; + var xIsEqual = Approximately(authorityScale.x, nonAuthorityScale.x); + var yIsEqual = Approximately(authorityScale.y, nonAuthorityScale.y); + var zIsEqual = Approximately(authorityScale.z, nonAuthorityScale.z); + if (!xIsEqual || !yIsEqual || !zIsEqual) + { + VerboseDebug($"[{transformA.gameObject.name}] Authority scale {authorityScale} != [{transformB.gameObject.name}] NonAuthority scale {nonAuthorityScale}"); + } + else if (m_EnableVerboseDebug) + { + VerboseDebug($"[{transformA.gameObject.name}] Authority scale {authorityScale} == [{transformB.gameObject.name}] NonAuthority scale {nonAuthorityScale}"); + } + return xIsEqual && yIsEqual && zIsEqual; + } + } } diff --git a/testproject/Assets/Tests/Runtime/NetworkSceneManager/NetworkObjectTestComponent.cs b/testproject/Assets/Tests/Runtime/NetworkSceneManager/NetworkObjectTestComponent.cs index b44c670ffa..f2ea87833d 100644 --- a/testproject/Assets/Tests/Runtime/NetworkSceneManager/NetworkObjectTestComponent.cs +++ b/testproject/Assets/Tests/Runtime/NetworkSceneManager/NetworkObjectTestComponent.cs @@ -12,6 +12,7 @@ namespace TestProject.RuntimeTests /// public class NetworkObjectTestComponent : NetworkBehaviour { + public static bool VerboseDebug; public static bool DisableOnDespawn; public static bool DisableOnSpawn; public static NetworkObject ServerNetworkObjectInstance; @@ -73,7 +74,7 @@ public override void OnNetworkDespawn() { OnInSceneObjectDespawned?.Invoke(NetworkObject); m_HasNotifiedSpawned = false; - Debug.Log($"{NetworkManager.name} de-spawned {gameObject.name}."); + LogMessage($"{NetworkManager.name} de-spawned {gameObject.name}."); SpawnedInstances.Remove(this); if (DisableOnDespawn) { @@ -89,9 +90,17 @@ private void Update() // We do this so the ObjectNameIdentifier has a chance to label it properly if (IsSpawned && !m_HasNotifiedSpawned) { - Debug.Log($"{NetworkManager.name} spawned {gameObject.name} with scene origin handle {gameObject.scene.handle}."); + LogMessage($"{NetworkManager.name} spawned {gameObject.name} with scene origin handle {gameObject.scene.handle}."); m_HasNotifiedSpawned = true; } } + + private void LogMessage(string message) + { + if (VerboseDebug) + { + Debug.Log(message); + } + } } } diff --git a/testproject/ProjectSettings/EditorBuildSettings.asset b/testproject/ProjectSettings/EditorBuildSettings.asset index d3782c9a5e..f9b564b5ea 100644 --- a/testproject/ProjectSettings/EditorBuildSettings.asset +++ b/testproject/ProjectSettings/EditorBuildSettings.asset @@ -143,6 +143,12 @@ EditorBuildSettings: - enabled: 1 path: Assets/Tests/Manual/PrefabTestAssets/PrefabTestScene.unity guid: 6449955dcdde54944ba1cdb97a23bd29 + - enabled: 1 + path: Assets/Tests/Manual/IntegrationTestScenes/InSceneUnderGameObject.unity + guid: 778ea62ad5daee5408d1ec1fce28673f + - enabled: 1 + path: Assets/Tests/Manual/IntegrationTestScenes/InSceneUnderGameObjectWithNT.unity + guid: 5a4f489df08d16c4d8c0167b099de2ca - enabled: 1 path: Assets/Tests/Manual/IntegrationTestScenes/SessionSynchronize.unity guid: 468b795904b98234593ebc31bf0d578a