Skip to content

Commit 4487baa

Browse files
feat: Nested NetworkBehaviour Synchronization [MTT-5024] (#2298)
* feat This includes the changes to provide users with a way to synchronize NetworkBehaviours with custom data prior to their associated NetworkObject is spawned. * fix and refactor This updates NetworkTransform so that it will properly synchronize when placed on nested NetworkBehaviours (i.e. their GameObject has no NetworkObject component). This also includes some fixes that allows for NetworkObjects to fail, NetworkVariables to fail, and NetworkBehaviour synchronization to fail without impacting the rest of the synchronization process. Minor fix for in-scene placed parenting under a non-NetworkObject to prevent from being added to the orphaned child list. * test Renamed NetworkObjectSceneSerializationTests to NetworkObjectSynchronizationTests. Added a more robust/wider range of tests, also added running host or server as well as added a basic OnSynchronize test. Added integration test to validate nested network transforms synchronize properly with nested GameObjects' transforms. Fixed an issue with late joined clients not registering all players in NetcodeIntegrationTest. * test manual This include a nested NetworkTransform manual test to visually validate the nested NetworkTransform update.
1 parent e242f1b commit 4487baa

36 files changed

+4681
-306
lines changed

com.unity.netcode.gameobjects/CHANGELOG.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,17 +9,23 @@ Additional documentation and release notes are available at [Multiplayer Documen
99
## [Unreleased]
1010

1111
### Added
12+
13+
- Added protected method `NetworkBehaviour.OnSynchronize` which is invoked during the initial `NetworkObject` synchronization process. This provides users the ability to include custom serialization information that will be applied to the `NetworkBehaviour` prior to the `NetworkObject` being spawned. (#2298)
1214
- Added support for different versions of the SDK to talk to each other in circumstances where changes permit it. Starting with this version and into future versions, patch versions should be compatible as long as the minor version is the same. (#2290)
1315
- Added `NetworkObject` auto-add helper and Multiplayer Tools install reminder settings to Project Settings. (#2285)
14-
- Added `public string DisconnectReason` getter to `NetworkManager` and `string Reason` to `ConnectionApprovalResponse`. Allows connection approval to communicate back a reason. Also added `public void DisconnectClient(ulong clientId, string reason)` allowing setting a disconnection reason, when explicitly disconnecting a client.
16+
- Added `public string DisconnectReason` getter to `NetworkManager` and `string Reason` to `ConnectionApprovalResponse`. Allows connection approval to communicate back a reason. Also added `public void DisconnectClient(ulong clientId, string reason)` allowing setting a disconnection reason, when explicitly disconnecting a client. (#2280)
1517

1618
### Changed
1719

1820
- Changed 3rd-party `XXHash` (32 & 64) implementation with an in-house reimplementation (#2310)
21+
- When `NetworkConfig.EnsureNetworkVariableLengthSafety` is disabled `NetworkVariable` fields do not write the additional `ushort` size value (_which helps to reduce the total synchronization message size_), but when enabled it still writes the additional `ushort` value. (#2298)
1922
- Optimized bandwidth usage by encoding most integer fields using variable-length encoding. (#2276)
2023

2124
### Fixed
2225

26+
- Fixed issue where `NetworkTransform` components nested under a parent with a `NetworkObject` component (i.e. network prefab) would not have their associated `GameObject`'s transform synchronized. (#2298)
27+
- Fixed issue where `NetworkObject`s that failed to instantiate could cause the entire synchronization pipeline to be disrupted/halted for a connecting client. (#2298)
28+
- Fixed issue where in-scene placed `NetworkObject`s nested under a `GameObject` would be added to the orphaned children list causing continual console warning log messages. (#2298)
2329
- Custom messages are now properly received by the local client when they're sent while running in host mode. (#2296)
2430
- Fixed issue where the host would receive more than one event completed notification when loading or unloading a scene only when no clients were connected. (#2292)
2531
- Fixed an issue in `UnityTransport` where an error would be logged if the 'Use Encryption' flag was enabled with a Relay configuration that used a secure protocol. (#2289)

com.unity.netcode.gameobjects/Components/NetworkTransform.cs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -451,6 +451,33 @@ internal NetworkTransformState GetLastSentState()
451451
return m_LastSentState;
452452
}
453453

454+
/// <summary>
455+
/// This is invoked when a new client joins (server and client sides)
456+
/// Server Side: Serializes as if we were teleporting (everything is sent via NetworkTransformState)
457+
/// Client Side: Adds the interpolated state which applies the NetworkTransformState as well
458+
/// </summary>
459+
protected override void OnSynchronize<T>(ref BufferSerializer<T> serializer)
460+
{
461+
// We don't need to synchronize NetworkTransforms that are on the same
462+
// GameObject as the NetworkObject.
463+
if (NetworkObject.gameObject == gameObject)
464+
{
465+
return;
466+
}
467+
var synchronizationState = new NetworkTransformState();
468+
if (serializer.IsWriter)
469+
{
470+
synchronizationState.IsTeleportingNextFrame = true;
471+
ApplyTransformToNetworkStateWithInfo(ref synchronizationState, m_CachedNetworkManager.LocalTime.Time, transform);
472+
synchronizationState.NetworkSerialize(serializer);
473+
}
474+
else
475+
{
476+
synchronizationState.NetworkSerialize(serializer);
477+
AddInterpolatedState(synchronizationState);
478+
}
479+
}
480+
454481
/// <summary>
455482
/// This will try to send/commit the current transform delta states (if any)
456483
/// </summary>

com.unity.netcode.gameobjects/Runtime/Core/NetworkBehaviour.cs

Lines changed: 185 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -763,6 +763,14 @@ internal void MarkVariablesDirty(bool dirty)
763763
}
764764
}
765765

766+
/// <summary>
767+
/// Synchronizes by setting only the NetworkVariable field values that the client has permission to read.
768+
/// Note: This is only invoked when first synchronizing a NetworkBehaviour (i.e. late join or spawned NetworkObject)
769+
/// </summary>
770+
/// <remarks>
771+
/// When NetworkConfig.EnsureNetworkVariableLengthSafety is enabled each NetworkVariable field will be preceded
772+
/// by the number of bytes written for that specific field.
773+
/// </remarks>
766774
internal void WriteNetworkVariableData(FastBufferWriter writer, ulong targetClientId)
767775
{
768776
if (NetworkVariableFields.Count == 0)
@@ -772,32 +780,47 @@ internal void WriteNetworkVariableData(FastBufferWriter writer, ulong targetClie
772780

773781
for (int j = 0; j < NetworkVariableFields.Count; j++)
774782
{
775-
bool canClientRead = NetworkVariableFields[j].CanClientRead(targetClientId);
776783

777-
if (canClientRead)
784+
if (NetworkVariableFields[j].CanClientRead(targetClientId))
778785
{
779-
var writePos = writer.Position;
780-
// Note: This value can't be packed because we don't know how large it will be in advance
781-
// we reserve space for it, then write the data, then come back and fill in the space
782-
// to pack here, we'd have to write data to a temporary buffer and copy it in - which
783-
// isn't worth possibly saving one byte if and only if the data is less than 63 bytes long...
784-
// The way we do packing, any value > 63 in a ushort will use the full 2 bytes to represent.
785-
writer.WriteValueSafe((ushort)0);
786-
var startPos = writer.Position;
787-
NetworkVariableFields[j].WriteField(writer);
788-
var size = writer.Position - startPos;
789-
writer.Seek(writePos);
790-
writer.WriteValueSafe((ushort)size);
791-
writer.Seek(startPos + size);
786+
if (NetworkManager.NetworkConfig.EnsureNetworkVariableLengthSafety)
787+
{
788+
var writePos = writer.Position;
789+
// Note: This value can't be packed because we don't know how large it will be in advance
790+
// we reserve space for it, then write the data, then come back and fill in the space
791+
// to pack here, we'd have to write data to a temporary buffer and copy it in - which
792+
// isn't worth possibly saving one byte if and only if the data is less than 63 bytes long...
793+
// The way we do packing, any value > 63 in a ushort will use the full 2 bytes to represent.
794+
writer.WriteValueSafe((ushort)0);
795+
var startPos = writer.Position;
796+
NetworkVariableFields[j].WriteField(writer);
797+
var size = writer.Position - startPos;
798+
writer.Seek(writePos);
799+
writer.WriteValueSafe((ushort)size);
800+
writer.Seek(startPos + size);
801+
}
802+
else
803+
{
804+
NetworkVariableFields[j].WriteField(writer);
805+
}
792806
}
793-
else
807+
else // Only if EnsureNetworkVariableLengthSafety, otherwise just skip
808+
if (NetworkManager.NetworkConfig.EnsureNetworkVariableLengthSafety)
794809
{
795810
writer.WriteValueSafe((ushort)0);
796811
}
797812
}
798813
}
799814

800-
internal void SetNetworkVariableData(FastBufferReader reader)
815+
/// <summary>
816+
/// Synchronizes by setting only the NetworkVariable field values that the client has permission to read.
817+
/// Note: This is only invoked when first synchronizing a NetworkBehaviour (i.e. late join or spawned NetworkObject)
818+
/// </summary>
819+
/// <remarks>
820+
/// When NetworkConfig.EnsureNetworkVariableLengthSafety is enabled each NetworkVariable field will be preceded
821+
/// by the number of bytes written for that specific field.
822+
/// </remarks>
823+
internal void SetNetworkVariableData(FastBufferReader reader, ulong clientId)
801824
{
802825
if (NetworkVariableFields.Count == 0)
803826
{
@@ -806,13 +829,23 @@ internal void SetNetworkVariableData(FastBufferReader reader)
806829

807830
for (int j = 0; j < NetworkVariableFields.Count; j++)
808831
{
809-
reader.ReadValueSafe(out ushort varSize);
810-
if (varSize == 0)
832+
var varSize = (ushort)0;
833+
var readStartPos = 0;
834+
if (NetworkManager.NetworkConfig.EnsureNetworkVariableLengthSafety)
835+
{
836+
reader.ReadValueSafe(out varSize);
837+
if (varSize == 0)
838+
{
839+
continue;
840+
}
841+
readStartPos = reader.Position;
842+
}
843+
else // If the client cannot read this field, then skip it
844+
if (!NetworkVariableFields[j].CanClientRead(clientId))
811845
{
812846
continue;
813847
}
814848

815-
var readStartPos = reader.Position;
816849
NetworkVariableFields[j].ReadField(reader);
817850

818851
if (NetworkManager.NetworkConfig.EnsureNetworkVariableLengthSafety)
@@ -849,6 +882,138 @@ protected NetworkObject GetNetworkObject(ulong networkId)
849882
return NetworkManager.SpawnManager.SpawnedObjects.TryGetValue(networkId, out NetworkObject networkObject) ? networkObject : null;
850883
}
851884

885+
/// <summary>
886+
/// Override this method if your derived NetworkBehaviour requires custom synchronization data.
887+
/// Note: Use of this method is only for the initial client synchronization of NetworkBehaviours
888+
/// and will increase the payload size for client synchronization and dynamically spawned
889+
/// <see cref="NetworkObject"/>s.
890+
/// </summary>
891+
/// <remarks>
892+
/// When serializing (writing) this will be invoked during the client synchronization period and
893+
/// when spawning new NetworkObjects.
894+
/// When deserializing (reading), this will be invoked prior to the NetworkBehaviour's associated
895+
/// NetworkObject being spawned.
896+
/// </remarks>
897+
/// <param name="serializer">The serializer to use to read and write the data.</param>
898+
/// <typeparam name="T">
899+
/// Either BufferSerializerReader or BufferSerializerWriter, depending whether the serializer
900+
/// is in read mode or write mode.
901+
/// </typeparam>
902+
protected virtual void OnSynchronize<T>(ref BufferSerializer<T> serializer) where T : IReaderWriter
903+
{
904+
905+
}
906+
907+
/// <summary>
908+
/// Internal method that determines if a NetworkBehaviour has additional synchronization data to
909+
/// be synchronized when first instantiated prior to its associated NetworkObject being spawned.
910+
/// </summary>
911+
/// <remarks>
912+
/// This includes try-catch blocks to recover from exceptions that might occur and continue to
913+
/// synchronize any remaining NetworkBehaviours.
914+
/// </remarks>
915+
/// <returns>true if it wrote synchronization data and false if it did not</returns>
916+
internal bool Synchronize<T>(ref BufferSerializer<T> serializer) where T : IReaderWriter
917+
{
918+
if (serializer.IsWriter)
919+
{
920+
// Get the writer to handle seeking and determining how many bytes were written
921+
var writer = serializer.GetFastBufferWriter();
922+
// Save our position before we attempt to write anything so we can seek back to it (i.e. error occurs)
923+
var positionBeforeWrite = writer.Position;
924+
writer.WriteValueSafe(NetworkBehaviourId);
925+
926+
// Save our position where we will write the final size being written so we can skip over it in the
927+
// event an exception occurs when deserializing.
928+
var sizePosition = writer.Position;
929+
writer.WriteValueSafe((ushort)0);
930+
931+
// Save our position before synchronizing to determine how much was written
932+
var positionBeforeSynchronize = writer.Position;
933+
var threwException = false;
934+
try
935+
{
936+
OnSynchronize(ref serializer);
937+
}
938+
catch (Exception ex)
939+
{
940+
threwException = true;
941+
if (NetworkManager.LogLevel <= LogLevel.Normal)
942+
{
943+
NetworkLog.LogWarning($"{name} threw an exception during synchronization serialization, this {nameof(NetworkBehaviour)} is being skipped and will not be synchronized!");
944+
if (NetworkManager.LogLevel == LogLevel.Developer)
945+
{
946+
NetworkLog.LogError($"{ex.Message}\n {ex.StackTrace}");
947+
}
948+
}
949+
}
950+
var finalPosition = writer.Position;
951+
952+
// If we wrote nothing then skip writing anything for this NetworkBehaviour
953+
if (finalPosition == positionBeforeSynchronize || threwException)
954+
{
955+
writer.Seek(positionBeforeWrite);
956+
return false;
957+
}
958+
else
959+
{
960+
// Write the number of bytes serialized to handle exceptions on the deserialization side
961+
var bytesWritten = finalPosition - positionBeforeSynchronize;
962+
writer.Seek(sizePosition);
963+
writer.WriteValueSafe((ushort)bytesWritten);
964+
writer.Seek(finalPosition);
965+
}
966+
return true;
967+
}
968+
else
969+
{
970+
var reader = serializer.GetFastBufferReader();
971+
// We will always read the expected byte count
972+
reader.ReadValueSafe(out ushort expectedBytesToRead);
973+
974+
// Save our position before we begin synchronization deserialization
975+
var positionBeforeSynchronize = reader.Position;
976+
var synchronizationError = false;
977+
try
978+
{
979+
// Invoke synchronization
980+
OnSynchronize(ref serializer);
981+
}
982+
catch (Exception ex)
983+
{
984+
if (NetworkManager.LogLevel <= LogLevel.Normal)
985+
{
986+
NetworkLog.LogWarning($"{name} threw an exception during synchronization deserialization, this {nameof(NetworkBehaviour)} is being skipped and will not be synchronized!");
987+
if (NetworkManager.LogLevel == LogLevel.Developer)
988+
{
989+
NetworkLog.LogError($"{ex.Message}\n {ex.StackTrace}");
990+
}
991+
}
992+
synchronizationError = true;
993+
}
994+
995+
var totalBytesRead = reader.Position - positionBeforeSynchronize;
996+
if (totalBytesRead != expectedBytesToRead)
997+
{
998+
if (NetworkManager.LogLevel <= LogLevel.Normal)
999+
{
1000+
NetworkLog.LogWarning($"{name} read {totalBytesRead} bytes but was expected to read {expectedBytesToRead} bytes during synchronization deserialization! This {nameof(NetworkBehaviour)} is being skipped and will not be synchronized!");
1001+
}
1002+
synchronizationError = true;
1003+
}
1004+
1005+
// Skip over the entry if deserialization fails
1006+
if (synchronizationError)
1007+
{
1008+
var skipToPosition = positionBeforeSynchronize + expectedBytesToRead;
1009+
reader.Seek(skipToPosition);
1010+
return false;
1011+
}
1012+
return true;
1013+
}
1014+
}
1015+
1016+
8521017
/// <summary>
8531018
/// Invoked when the <see cref="GameObject"/> the <see cref="NetworkBehaviour"/> is attached to.
8541019
/// NOTE: If you override this, you will want to always invoke this base class version of this

0 commit comments

Comments
 (0)