com.unity.netcode.gameobjects@2.2.0

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).

## [2.2.0] - 2024-12-12

### Added

- Added `NetworkObject.OwnershipStatus.SessionOwner` to allow Network Objects to be distributable and only owned by the Session Owner. This flag will override all other `OwnershipStatus` flags. (#3175)
- Added `UnityTransport.GetEndpoint` method to provide a way to obtain `NetworkEndpoint` information of a connection via client identifier. (#3130)
- Added `NetworkTransport.OnEarlyUpdate` and `NetworkTransport.OnPostLateUpdate` methods to provide more control over handling transport related events at the start and end of each frame. (#3113)

### Fixed

- Fixed issue where the server, host, or session owner would not populate the in-scene place `NetworkObject` table if the scene was loaded prior to starting the `NetworkManager`. (#3177)
- Fixed issue where the `NetworkObjectIdHash` value could be incorrect when entering play mode while still in prefab edit mode with pending changes and using MPPM. (#3162)
- Fixed issue where a sever only `NetworkManager` instance would spawn the actual `NetworkPrefab`'s `GameObject` as opposed to creating an instance of it. (#3160)
- Fixed issue where only the session owner (as opposed to all clients) would handle spawning prefab overrides properly when using a distributed authority network topology. (#3160)
- Fixed issue where an exception was thrown when calling `NetworkManager.Shutdown` after calling `UnityTransport.Shutdown`. (#3118)
- Fixed issue where `NetworkList` properties on in-scene placed `NetworkObject`s could cause small memory leaks when entering playmode. (#3147)
- Fixed in-scene `NertworkObject` synchronization issue when loading a scene with currently connected clients connected to a session created by a `NetworkManager` started as a server (i.e. not as a host). (#3133)
- Fixed issue where a `NetworkManager` started as a server would not add itself as an observer to in-scene placed `NetworkObject`s instantiated and spawned by a scene loading event. (#3133)
- Fixed issue where spawning a player using `NetworkObject.InstantiateAndSpawn` or `NetworkSpawnManager.InstantiateAndSpawn` would not update the `NetworkSpawnManager.PlayerObjects` or assign the newly spawned player to the `NetworkClient.PlayerObject`. (#3122)
- Fixed issue where queued UnitTransport (NetworkTransport) message batches were being sent on the next frame. They are now sent at the end of the frame during `PostLateUpdate`.  (#3113)
- Fixed issue where `NotOwnerRpcTarget` or `OwnerRpcTarget` were not using their replacements `NotAuthorityRpcTarget` and `AuthorityRpcTarget` which would invoke a warning. (#3111)
- Fixed issue where client is removed as an observer from spawned objects when their player instance is despawned. (#3110)
- Fixed issue where `NetworkAnimator` would statically allocate write buffer space for `Animator` parameters that could cause a write error if the number of parameters exceeded the space allocated. (#3108)

### Changed

- In-scene placed `NetworkObject`s have been made distributable when balancing object distribution after a connection event. (#3175)
- Optimised `NetworkVariable` and `NetworkTransform` related packets when in Distributed Authority mode.
- The Debug Simulator section of the Unity Transport component was removed. This section was not functional anymore and users are now recommended to use the more featureful [Network Simulator](https://docs-multiplayer.unity3d.com/tools/current/tools-network-simulator/) tool from the Multiplayer Tools package instead. (#3121)
This commit is contained in:
Unity Technologies
2024-12-12 00:00:00 +00:00
parent 016788c21e
commit 8fe07bbad2
78 changed files with 2243 additions and 1187 deletions

View File

@@ -88,6 +88,13 @@ namespace Unity.Netcode
}
}
// Only if spawn with observers is set or we are using a distributed authority network topology and this is the client's player should we add
// the owner as an observer.
if (playerObject.SpawnWithObservers || (NetworkManager.DistributedAuthorityMode && NetworkManager.LocalClientId == playerObject.OwnerClientId))
{
playerObject.Observers.Add(playerObject.OwnerClientId);
}
m_PlayerObjects.Add(playerObject);
if (!m_PlayerObjectsTable.ContainsKey(playerObject.OwnerClientId))
{
@@ -110,8 +117,9 @@ namespace Unity.Netcode
if (playerNetworkClient.PlayerObject != null && m_PlayerObjects.Contains(playerNetworkClient.PlayerObject))
{
// Just remove the previous player object but keep the assigned observers of the NetworkObject
RemovePlayerObject(playerNetworkClient.PlayerObject, true);
RemovePlayerObject(playerNetworkClient.PlayerObject);
}
// Now update the associated NetworkClient's player object
NetworkManager.ConnectionManager.ConnectedClients[playerObject.OwnerClientId].AssignPlayerObject(ref playerObject);
AddPlayerObject(playerObject);
@@ -120,7 +128,7 @@ namespace Unity.Netcode
/// <summary>
/// Removes a player object and updates all other players' observers list
/// </summary>
private void RemovePlayerObject(NetworkObject playerObject, bool keepObservers = false)
private void RemovePlayerObject(NetworkObject playerObject, bool destroyingObject = false)
{
if (!playerObject.IsPlayerObject)
{
@@ -141,16 +149,21 @@ namespace Unity.Netcode
}
}
// If we want to keep the observers, then exit early
if (keepObservers)
if (NetworkManager.ConnectionManager.ConnectedClients.ContainsKey(playerObject.OwnerClientId) && destroyingObject)
{
return;
NetworkManager.ConnectionManager.ConnectedClients[playerObject.OwnerClientId].PlayerObject = null;
}
foreach (var player in m_PlayerObjects)
{
player.Observers.Remove(playerObject.OwnerClientId);
}
// If we want to keep the observers, then exit early
//if (keepObservers)
//{
// return;
//}
//foreach (var player in m_PlayerObjects)
//{
// player.Observers.Remove(playerObject.OwnerClientId);
//}
}
internal void MarkObjectForShowingTo(NetworkObject networkObject, ulong clientId)
@@ -409,27 +422,8 @@ namespace Unity.Netcode
{
if (NetworkManager.DistributedAuthorityMode && !NetworkManager.ShutdownInProgress)
{
if (networkObject.IsOwnershipDistributable || networkObject.IsOwnershipTransferable)
{
if (networkObject.IsOwner || NetworkManager.DAHost)
{
NetworkLog.LogWarning("DANGO-TODO: Determine if removing ownership should make the CMB Service redistribute ownership or if this just isn't a valid thing in DAMode.");
return;
}
else
{
NetworkLog.LogError($"Only the owner is allowed to remove ownership in distributed authority mode!");
return;
}
}
else
{
if (!NetworkManager.DAHost)
{
Debug.LogError($"Only {nameof(NetworkObject)}s with {nameof(NetworkObject.IsOwnershipDistributable)} or {nameof(NetworkObject.IsOwnershipTransferable)} set can perform ownership changes!");
}
return;
}
Debug.LogError($"Removing ownership is invalid in Distributed Authority Mode. Use {nameof(ChangeOwnership)} instead.");
return;
}
ChangeOwnership(networkObject, NetworkManager.ServerClientId, true);
}
@@ -461,6 +455,18 @@ namespace Unity.Netcode
if (NetworkManager.DistributedAuthorityMode)
{
// Ensure only the session owner can change ownership (i.e. acquire) and that the session owner is not trying to assign a non-session owner client
// ownership of a NetworkObject with SessionOwner permissions.
if (networkObject.IsOwnershipSessionOwner && (!NetworkManager.LocalClient.IsSessionOwner || clientId != NetworkManager.CurrentSessionOwner))
{
if (NetworkManager.LogLevel <= LogLevel.Developer)
{
NetworkLog.LogErrorServer($"[{networkObject.name}][Session Owner Only] You cannot change ownership of a {nameof(NetworkObject)} that has the {NetworkObject.OwnershipStatus.SessionOwner} flag set!");
}
networkObject.OnOwnershipPermissionsFailure?.Invoke(NetworkObject.OwnershipPermissionsFailureStatus.SessionOwnerOnly);
return;
}
// If are not authorized and this is not an approved ownership change, then check to see if we can change ownership
if (!isAuthorized && !isRequestApproval)
{
@@ -567,7 +573,6 @@ namespace Unity.Netcode
{
networkObject.ChildNetworkBehaviours[i].UpdateNetworkProperties();
}
size = NetworkManager.ConnectionManager.SendMessage(ref message, NetworkDelivery.ReliableSequenced, NetworkManager.ServerClientId);
NetworkManager.NetworkMetrics.TrackOwnershipChangeSent(NetworkManager.LocalClientId, networkObject, size);
}
@@ -724,12 +729,20 @@ namespace Unity.Netcode
internal NetworkObject InstantiateAndSpawnNoParameterChecks(NetworkObject networkPrefab, ulong ownerClientId = NetworkManager.ServerClientId, bool destroyWithScene = false, bool isPlayerObject = false, bool forceOverride = false, Vector3 position = default, Quaternion rotation = default)
{
var networkObject = networkPrefab;
// Host spawns the ovveride and server spawns the original prefab unless forceOverride is set to true where both server or host will spawn the override.
// In distributed authority mode, we alaways get the override
if (forceOverride || NetworkManager.IsHost || NetworkManager.DistributedAuthorityMode)
// - Host and clients always instantiate the override if one exists.
// - Server instantiates the original prefab unless:
// -- forceOverride is set to true =or=
// -- The prefab has a registered prefab handler, then we let user code determine what to spawn.
// - Distributed authority mode always spawns the override if one exists.
if (forceOverride || NetworkManager.IsClient || NetworkManager.DistributedAuthorityMode || NetworkManager.PrefabHandler.ContainsHandler(networkPrefab.GlobalObjectIdHash))
{
networkObject = GetNetworkObjectToSpawn(networkPrefab.GlobalObjectIdHash, ownerClientId, position, rotation);
}
else // Under this case, server instantiate the prefab passed in.
{
networkObject = InstantiateNetworkPrefab(networkPrefab.gameObject, networkPrefab.GlobalObjectIdHash, position, rotation);
}
if (networkObject == null)
{
Debug.LogError($"Failed to instantiate and spawn {networkPrefab.name}!");
@@ -738,12 +751,20 @@ namespace Unity.Netcode
networkObject.IsPlayerObject = isPlayerObject;
networkObject.transform.position = position;
networkObject.transform.rotation = rotation;
networkObject.SpawnWithOwnership(ownerClientId, destroyWithScene);
// If spawning as a player, then invoke SpawnAsPlayerObject
if (isPlayerObject)
{
networkObject.SpawnAsPlayerObject(ownerClientId, destroyWithScene);
}
else // Otherwise just spawn with ownership
{
networkObject.SpawnWithOwnership(ownerClientId, destroyWithScene);
}
return networkObject;
}
/// <summary>
/// Gets the right NetworkObject prefab instance to spawn. If a handler is registered or there is an override assigned to the
/// Gets the right NetworkObject prefab instance to spawn. If a handler is registered or there is an override assigned to the
/// passed in globalObjectIdHash value, then that is what will be instantiated, spawned, and returned.
/// </summary>
internal NetworkObject GetNetworkObjectToSpawn(uint globalObjectIdHash, ulong ownerId, Vector3? position, Quaternion? rotation, bool isScenePlaced = false)
@@ -775,8 +796,8 @@ namespace Unity.Netcode
case NetworkPrefabOverride.Hash:
case NetworkPrefabOverride.Prefab:
{
// When scene management is disabled and this is an in-scene placed NetworkObject, we want to always use the
// SourcePrefabToOverride and not any possible prefab override as a user might want to spawn overrides dynamically
// When scene management is disabled and this is an in-scene placed NetworkObject, we want to always use the
// SourcePrefabToOverride and not any possible prefab override as a user might want to spawn overrides dynamically
// but might want to use the same source network prefab as an in-scene placed NetworkObject.
// (When scene management is enabled, clients don't delete their in-scene placed NetworkObjects prior to dynamically
// spawning them so the original prefab placed is preserved and this is not needed)
@@ -804,16 +825,37 @@ namespace Unity.Netcode
else
{
// Create prefab instance while applying any pre-assigned position and rotation values
networkObject = UnityEngine.Object.Instantiate(networkPrefabReference).GetComponent<NetworkObject>();
networkObject.transform.position = position ?? networkObject.transform.position;
networkObject.transform.rotation = rotation ?? networkObject.transform.rotation;
networkObject.NetworkManagerOwner = NetworkManager;
networkObject.PrefabGlobalObjectIdHash = globalObjectIdHash;
networkObject = InstantiateNetworkPrefab(networkPrefabReference, globalObjectIdHash, position, rotation);
}
}
return networkObject;
}
/// <summary>
/// Instantiates a network prefab instance, assigns the base prefab <see cref="NetworkObject.GlobalObjectIdHash"/>, positions, and orients
/// the instance.
/// !!! Should only be invoked by <see cref="GetNetworkObjectToSpawn"/> unless used by an integration test !!!
/// </summary>
/// <remarks>
/// <param name="prefabGlobalObjectIdHash"> should be the base prefab <see cref="NetworkObject.GlobalObjectIdHash"/> value and not the
/// overrided value.
/// (Can be used for integration testing)
/// </remarks>
/// <param name="networkPrefab">prefab to instantiate</param>
/// <param name="prefabGlobalObjectIdHash"><see cref="NetworkObject.GlobalObjectIdHash"/> of the base prefab instance</param>
/// <param name="position">conditional position in place of the network prefab's default position</param>
/// <param name="rotation">conditional rotation in place of the network prefab's default rotation</param>
/// <returns>the instance of the <see cref="NetworkObject"/></returns>
internal NetworkObject InstantiateNetworkPrefab(GameObject networkPrefab, uint prefabGlobalObjectIdHash, Vector3? position, Quaternion? rotation)
{
var networkObject = UnityEngine.Object.Instantiate(networkPrefab).GetComponent<NetworkObject>();
networkObject.transform.position = position ?? networkObject.transform.position;
networkObject.transform.rotation = rotation ?? networkObject.transform.rotation;
networkObject.NetworkManagerOwner = NetworkManager;
networkObject.PrefabGlobalObjectIdHash = prefabGlobalObjectIdHash;
return networkObject;
}
/// <summary>
/// Creates a local NetowrkObject to be spawned.
/// </summary>
@@ -949,7 +991,7 @@ namespace Unity.Netcode
/// - NetworkObject when spawning a newly instantiated NetworkObject for the first time.
/// - NetworkSceneManager after a server/session-owner has loaded a scene to locally spawn the newly instantiated in-scene placed NetworkObjects.
/// - NetworkSpawnManager when spawning any already loaded in-scene placed NetworkObjects (client-server or session owner).
///
///
/// Client-Server:
/// Server is the only instance that invokes this method.
///
@@ -1098,6 +1140,12 @@ namespace Unity.Netcode
// 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.Observers.Add(NetworkManager.LocalClientId);
}
// Add client observers
for (int i = 0; i < NetworkManager.ConnectedClientsIds.Count; i++)
{
@@ -1321,7 +1369,7 @@ namespace Unity.Netcode
}
}
// If spawned, then despawn and potentially destroy.
// If spawned, then despawn and potentially destroy.
if (networkObjects[i].IsSpawned)
{
OnDespawnObject(networkObjects[i], shouldDestroy);
@@ -1401,6 +1449,15 @@ namespace Unity.Netcode
}
}
// Since we are spawing in-scene placed NetworkObjects for already loaded scenes,
// we need to add any in-scene placed NetworkObject to our tracking table
var clearFirst = true;
foreach (var sceneLoaded in NetworkManager.SceneManager.ScenesLoaded)
{
NetworkManager.SceneManager.PopulateScenePlacedObjects(sceneLoaded.Value, clearFirst);
clearFirst = false;
}
// Notify all in-scene placed NetworkObjects have been spawned
foreach (var networkObject in networkObjectsToSpawn)
{
@@ -1550,23 +1607,9 @@ namespace Unity.Netcode
SpawnedObjectsList.Remove(networkObject);
}
// DANGO-TODO: When we fix the issue with observers not being applied to NetworkObjects,
// (client connect/disconnect) we can remove this hacky way of doing this.
// Basically, when a player disconnects and/or is destroyed they are removed as an observer from all other client
// NetworkOject instances.
if (networkObject.IsPlayerObject && !networkObject.IsOwner && networkObject.OwnerClientId != NetworkManager.LocalClientId)
{
foreach (var netObject in SpawnedObjects)
{
if (netObject.Value.Observers.Contains(networkObject.OwnerClientId))
{
netObject.Value.Observers.Remove(networkObject.OwnerClientId);
}
}
}
if (networkObject.IsPlayerObject)
{
RemovePlayerObject(networkObject);
RemovePlayerObject(networkObject, destroyGameObject);
}
// Always clear out the observers list when despawned
@@ -1705,6 +1748,11 @@ namespace Unity.Netcode
foreach (var networkObject in NetworkManager.SpawnManager.SpawnedObjectsList)
{
if (networkObject.IsOwnershipSessionOwner)
{
continue;
}
if (networkObject.IsOwnershipDistributable && !networkObject.IsOwnershipLocked)
{
if (networkObject.transform.parent != null)
@@ -1715,17 +1763,15 @@ namespace Unity.Netcode
continue;
}
}
// We have to check if it is an in-scene placed NetworkObject and if it is get the source prefab asset GlobalObjectIdHash value of the in-scene placed instance
// since all in-scene placed instances use unique GlobalObjectIdHash values.
var globalOjectIdHash = networkObject.IsSceneObject.HasValue && networkObject.IsSceneObject.Value ? networkObject.InScenePlacedSourceGlobalObjectIdHash : networkObject.GlobalObjectIdHash;
if (networkObject.IsSceneObject.Value)
if (!objectTypeCount.ContainsKey(globalOjectIdHash))
{
continue;
objectTypeCount.Add(globalOjectIdHash, 0);
}
if (!objectTypeCount.ContainsKey(networkObject.GlobalObjectIdHash))
{
objectTypeCount.Add(networkObject.GlobalObjectIdHash, 0);
}
objectTypeCount[networkObject.GlobalObjectIdHash] += 1;
objectTypeCount[globalOjectIdHash] += 1;
// DANGO-TODO-MVP: Remove this once the service handles object distribution
if (onlyIncludeOwnedObjects && !networkObject.IsOwner)
@@ -1733,133 +1779,141 @@ namespace Unity.Netcode
continue;
}
// Divide up by prefab type (GlobalObjectIdHash) to get a better distribution of object types
if (!objectByTypeAndOwner.ContainsKey(networkObject.GlobalObjectIdHash))
if (!objectByTypeAndOwner.ContainsKey(globalOjectIdHash))
{
objectByTypeAndOwner.Add(networkObject.GlobalObjectIdHash, new Dictionary<ulong, List<NetworkObject>>());
objectByTypeAndOwner.Add(globalOjectIdHash, new Dictionary<ulong, List<NetworkObject>>());
}
// Sub-divide each type by owner
if (!objectByTypeAndOwner[networkObject.GlobalObjectIdHash].ContainsKey(networkObject.OwnerClientId))
if (!objectByTypeAndOwner[globalOjectIdHash].ContainsKey(networkObject.OwnerClientId))
{
objectByTypeAndOwner[networkObject.GlobalObjectIdHash].Add(networkObject.OwnerClientId, new List<NetworkObject>());
objectByTypeAndOwner[globalOjectIdHash].Add(networkObject.OwnerClientId, new List<NetworkObject>());
}
// Add to the client's spawned object list
objectByTypeAndOwner[networkObject.GlobalObjectIdHash][networkObject.OwnerClientId].Add(networkObject);
objectByTypeAndOwner[globalOjectIdHash][networkObject.OwnerClientId].Add(networkObject);
}
}
}
internal void DistributeNetworkObjects(ulong clientId)
{
// Distributed authority mode ownership distribution
// DANGO-TODO-MVP: Remove the session owner object distribution check once the service handles object distribution
if (NetworkManager.DistributedAuthorityMode && (NetworkManager.DAHost || NetworkManager.CMBServiceConnection))
if (!NetworkManager.DistributedAuthorityMode)
{
// DA-NGO CMB SERVICE NOTES:
// The most basic object distribution should be broken up into a table of spawned object types
// where each type contains a list of each client's owned objects of that type that can be
// distributed.
// The table format:
// [GlobalObjectIdHashValue][ClientId][List of Owned Objects]
var distributedNetworkObjects = new Dictionary<uint, Dictionary<ulong, List<NetworkObject>>>();
return;
}
// DA-NGO CMB SERVICE NOTES:
// This is optional, but I found it easier to get the total count of spawned objects for each prefab
// type contained in the previous table in order to be able to calculate the targeted object distribution
// count of that type per client.
var objectTypeCount = new Dictionary<uint, int>();
if (NetworkManager.SessionConfig.ServiceSideDistribution)
{
return;
}
// Get all spawned objects by type and then by client owner that are spawned and can be distributed
GetObjectDistribution(ref distributedNetworkObjects, ref objectTypeCount);
var clientCount = NetworkManager.ConnectedClientsIds.Count;
// DA-NGO CMB SERVICE NOTES:
// The most basic object distribution should be broken up into a table of spawned object types
// where each type contains a list of each client's owned objects of that type that can be
// distributed.
// The table format:
// [GlobalObjectIdHashValue][ClientId][List of Owned Objects]
var distributedNetworkObjects = new Dictionary<uint, Dictionary<ulong, List<NetworkObject>>>();
// Cycle through each prefab type
foreach (var objectTypeEntry in distributedNetworkObjects)
// DA-NGO CMB SERVICE NOTES:
// This is optional, but I found it easier to get the total count of spawned objects for each prefab
// type contained in the previous table in order to be able to calculate the targeted object distribution
// count of that type per client.
var objectTypeCount = new Dictionary<uint, int>();
// Get all spawned objects by type and then by client owner that are spawned and can be distributed
GetObjectDistribution(ref distributedNetworkObjects, ref objectTypeCount);
var clientCount = NetworkManager.ConnectedClientsIds.Count;
// Cycle through each prefab type
foreach (var objectTypeEntry in distributedNetworkObjects)
{
// Calculate the number of objects that should be distributed amongst the clients
var totalObjectsToDistribute = objectTypeCount[objectTypeEntry.Key];
var objPerClientF = totalObjectsToDistribute * (1.0f / clientCount);
var floorValue = (int)Math.Floor(objPerClientF);
var fractional = objPerClientF - floorValue;
var objPerClient = 0;
if (fractional >= 0.556f)
{
// Calculate the number of objects that should be distributed amongst the clients
var totalObjectsToDistribute = objectTypeCount[objectTypeEntry.Key];
var objPerClientF = totalObjectsToDistribute * (1.0f / clientCount);
var floorValue = (int)Math.Floor(objPerClientF);
var fractional = objPerClientF - floorValue;
var objPerClient = 0;
if (fractional >= 0.556f)
{
objPerClient = (int)Math.Round(totalObjectsToDistribute * (1.0f / clientCount));
}
else
{
objPerClient = floorValue;
}
objPerClient = (int)Math.Round(totalObjectsToDistribute * (1.0f / clientCount));
}
else
{
objPerClient = floorValue;
}
// If the object per client count is zero, then move to the next type.
if (objPerClient <= 0)
// If the object per client count is zero, then move to the next type.
if (objPerClient <= 0)
{
continue;
}
// Evenly distribute this object type amongst the clients
foreach (var ownerList in objectTypeEntry.Value)
{
if (ownerList.Value.Count <= 1)
{
continue;
}
// Evenly distribute this object type amongst the clients
foreach (var ownerList in objectTypeEntry.Value)
var maxDistributeCount = Mathf.Max(ownerList.Value.Count - objPerClient, 1);
var distributed = 0;
// For now when we have more players then distributed NetworkObjects that
// a specific client owns, just assign half of the NetworkObjects to the new client
var offsetCount = Mathf.Max((int)Math.Round((float)(ownerList.Value.Count / objPerClient)), 1);
if (EnableDistributeLogging)
{
if (ownerList.Value.Count <= 1)
{
continue;
}
Debug.Log($"[{objPerClient} of {totalObjectsToDistribute}][Client-{ownerList.Key}] Count: {ownerList.Value.Count} | ObjPerClient: {objPerClient} | maxD: {maxDistributeCount} | Offset: {offsetCount}");
}
var maxDistributeCount = Mathf.Max(ownerList.Value.Count - objPerClient, 1);
var distributed = 0;
// For now when we have more players then distributed NetworkObjects that
// a specific client owns, just assign half of the NetworkObjects to the new client
var offsetCount = Mathf.Max((int)Math.Round((float)(ownerList.Value.Count / objPerClient)), 1);
if (EnableDistributeLogging)
for (int i = 0; i < ownerList.Value.Count; i++)
{
if ((i % offsetCount) == 0)
{
Debug.Log($"[{objPerClient} of {totalObjectsToDistribute}][Client-{ownerList.Key}] Count: {ownerList.Value.Count} | ObjPerClient: {objPerClient} | maxD: {maxDistributeCount} | Offset: {offsetCount}");
}
for (int i = 0; i < ownerList.Value.Count; i++)
{
if ((i % offsetCount) == 0)
ChangeOwnership(ownerList.Value[i], clientId, true);
//if (EnableDistributeLogging)
{
ChangeOwnership(ownerList.Value[i], clientId, true);
if (EnableDistributeLogging)
{
Debug.Log($"[Client-{ownerList.Key}][NetworkObjectId-{ownerList.Value[i].NetworkObjectId} Distributed to Client-{clientId}");
}
distributed++;
}
if (distributed == maxDistributeCount)
{
break;
Debug.Log($"[Client-{ownerList.Key}][NetworkObjectId-{ownerList.Value[i].NetworkObjectId} Distributed to Client-{clientId}");
}
distributed++;
}
if (distributed == maxDistributeCount)
{
break;
}
}
}
// If EnableDistributeLogging is enabled, log the object type distribution counts per client
if (EnableDistributeLogging)
{
var builder = new StringBuilder();
distributedNetworkObjects.Clear();
objectTypeCount.Clear();
GetObjectDistribution(ref distributedNetworkObjects, ref objectTypeCount);
builder.AppendLine($"Client Relative Distributed Object Count: (distribution follows)");
// Cycle through each prefab type
foreach (var objectTypeEntry in distributedNetworkObjects)
{
builder.AppendLine($"[GID: {objectTypeEntry.Key} | {objectTypeEntry.Value.First().Value.First().name}][Total Count: {objectTypeCount[objectTypeEntry.Key]}]");
builder.AppendLine($"[GID: {objectTypeEntry.Key} | {objectTypeEntry.Value.First().Value.First().name}] Distribution:");
// Evenly distribute this type amongst clients
foreach (var ownerList in objectTypeEntry.Value)
{
builder.AppendLine($"[Client-{ownerList.Key}] Count: {ownerList.Value.Count}");
}
}
Debug.Log(builder.ToString());
}
}
// If EnableDistributeLogging is enabled, log the object type distribution counts per client
if (EnableDistributeLogging)
{
var builder = new StringBuilder();
distributedNetworkObjects.Clear();
objectTypeCount.Clear();
GetObjectDistribution(ref distributedNetworkObjects, ref objectTypeCount);
builder.AppendLine($"Client Relative Distributed Object Count: (distribution follows)");
// Cycle through each prefab type
foreach (var objectTypeEntry in distributedNetworkObjects)
{
builder.AppendLine($"[GID: {objectTypeEntry.Key} | {objectTypeEntry.Value.First().Value.First().name}][Total Count: {objectTypeCount[objectTypeEntry.Key]}]");
builder.AppendLine($"[GID: {objectTypeEntry.Key} | {objectTypeEntry.Value.First().Value.First().name}] Distribution:");
// Evenly distribute this type amongst clients
foreach (var ownerList in objectTypeEntry.Value)
{
builder.AppendLine($"[Client-{ownerList.Key}] Count: {ownerList.Value.Count}");
}
}
Debug.Log(builder.ToString());
}
}
internal struct DeferredDespawnObject
@@ -1969,14 +2023,14 @@ namespace Unity.Netcode
/// synchronizing in order to "show" (spawn) anything that might be currently hidden from
/// the session owner.
/// </summary>
/// <remarks>
/// Replacement is: SynchronizeObjectsToNewlyJoinedClient
/// </remarks>
internal void ShowHiddenObjectsToNewlyJoinedClient(ulong newClientId)
{
if (!NetworkManager.DistributedAuthorityMode)
if (NetworkManager == null || NetworkManager.ShutdownInProgress && NetworkManager.LogLevel <= LogLevel.Developer)
{
if (NetworkManager == null || !NetworkManager.ShutdownInProgress && NetworkManager.LogLevel <= LogLevel.Developer)
{
Debug.LogWarning($"[Internal Error] {nameof(ShowHiddenObjectsToNewlyJoinedClient)} invoked while !");
}
Debug.LogWarning($"[Internal Error] {nameof(ShowHiddenObjectsToNewlyJoinedClient)} invoked while shutdown is in progress!");
return;
}
@@ -2012,5 +2066,46 @@ namespace Unity.Netcode
}
}
}
internal void SynchronizeObjectsToNewlyJoinedClient(ulong newClientId)
{
if (NetworkManager == null || NetworkManager.ShutdownInProgress && NetworkManager.LogLevel <= LogLevel.Developer)
{
Debug.LogWarning($"[Internal Error] {nameof(SynchronizeObjectsToNewlyJoinedClient)} invoked while shutdown is in progress!");
return;
}
if (!NetworkManager.DistributedAuthorityMode)
{
Debug.LogError($"[Internal Error] {nameof(SynchronizeObjectsToNewlyJoinedClient)} should only be invoked when using a distributed authority network topology!");
return;
}
if (NetworkManager.NetworkConfig.EnableSceneManagement)
{
Debug.LogError($"[Internal Error] {nameof(SynchronizeObjectsToNewlyJoinedClient)} should only be invoked when scene management is disabled!");
return;
}
var localClientId = NetworkManager.LocalClient.ClientId;
foreach (var networkObject in SpawnedObjectsList)
{
if (networkObject.SpawnWithObservers && networkObject.OwnerClientId == localClientId)
{
if (networkObject.Observers.Contains(newClientId))
{
if (NetworkManager.LogLevel <= LogLevel.Developer)
{
// Temporary tracking to make sure we are not showing something already visibile (should never be the case for this)
Debug.LogWarning($"[{nameof(SynchronizeObjectsToNewlyJoinedClient)}][{networkObject.name}] New client as already an observer!");
}
// For now, remove the client (impossible for the new client to have an instance since the session owner doesn't) to make sure newly added
// code to handle this edge case works.
networkObject.Observers.Remove(newClientId);
}
networkObject.NetworkShow(newClientId);
}
}
}
}
}