Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Adds nested NetworkObject support in prefabs #2645

Open
wants to merge 10 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions com.unity.netcode.gameobjects/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ Additional documentation and release notes are available at [Multiplayer Documen

### Added

- Added the ability to spawn prefabs with nested NetworkObjects

### Fixed

- Fixed issue where `NetworkClient.OwnedObjects` was not returning any owned objects due to the `NetworkClient.IsConnected` not being properly set. (#2631)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -614,6 +614,11 @@ internal void HandleConnectionApproval(ulong ownerClientId, NetworkManager.Conne
ownerClientId,
destroyWithScene: false);

foreach (var dependingNetworkObject in networkObject.DependingNetworkObjects)
{
NetworkManager.SpawnManager.SpawnNetworkObjectLocally(dependingNetworkObject, NetworkManager.SpawnManager.GetNetworkObjectId(), false, false, ownerClientId, false);
}

client.AssignPlayerObject(ref networkObject);
}

Expand Down
256 changes: 228 additions & 28 deletions com.unity.netcode.gameobjects/Runtime/Core/NetworkObject.cs

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ public void Serialize(FastBufferWriter writer, int targetVersion)
// Serialize NetworkVariable data
foreach (var sobj in SpawnedObjectsList)
{
// Depending Network Objects will be spawned by their Dependent Network Object
if (sobj.DependentNetworkObject != null) { continue; }

if (sobj.CheckObjectVisibility == null || sobj.CheckObjectVisibility(OwnerClientId))
{
sobj.Observers.Add(OwnerClientId);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,7 @@ internal void AddSpawnedNetworkObjects()
m_NetworkObjectsSync.Clear();
foreach (var sobj in m_NetworkManager.SpawnManager.SpawnedObjectsList)
{
if (sobj.Observers.Contains(TargetClientId))
if (sobj.Observers.Contains(TargetClientId) && !sobj.IsDependent)
{
m_NetworkObjectsSync.Add(sobj);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -473,6 +473,46 @@ internal NetworkObject CreateLocalNetworkObject(NetworkObject.SceneObject sceneO
{
UnityEngine.Object.DontDestroyOnLoad(networkObject.gameObject);
}

// Hook up NetworkObjects that depend on this NetworkObject. Usually used for nested NetworkObjects in prefabs,
if (sceneObject.DependingObjects != null)
{
var DependingNetworkObjects = networkObject.DependingNetworkObjects;
for (int i = 0; i < sceneObject.DependingObjects.Length; i++)
{
var childData = sceneObject.DependingObjects[i];
var childNetworkObject = DependingNetworkObjects[i];

if (childData.IsSpawned)
{
childNetworkObject.DestroyWithScene = sceneObject.DestroyWithScene;
childNetworkObject.NetworkSceneHandle = sceneObject.NetworkSceneHandle;

// If a dependent NetworkObject does not have the HasParent flag set, it needs to be unparented
if (!sceneObject.HasParent && childNetworkObject.transform.parent != null)
{
childNetworkObject.ApplyNetworkParenting(true, true);
}

if (childData.HasParent)
{
// Go ahead and set network parenting properties, if the latest parent is not set then pass in null
// (we always want to set worldPositionStays)
ulong? parentId = null;
if (childData.IsLatestParentSet)
{
parentId = childData.HasParent ? childData.ParentObjectId : default;
}
childNetworkObject.SetNetworkParenting(parentId, true);
}
}
else
{
// Remove unspawned child NetworkObjects
GameObject.Destroy(networkObject.DependingNetworkObjects[i].gameObject);
}
}
}
}
return networkObject;
}
Expand All @@ -490,15 +530,6 @@ internal void SpawnNetworkObjectLocally(NetworkObject networkObject, ulong netwo
throw new SpawnStateException("Object is already spawned");
}

if (!sceneObject)
{
var networkObjectChildren = networkObject.GetComponentsInChildren<NetworkObject>();
if (networkObjectChildren.Length > 1)
{
Debug.LogError("Spawning NetworkObjects with nested NetworkObjects is only supported for scene objects. Child NetworkObjects will not be spawned over the network!");
}
}

SpawnNetworkObjectLocallyCommon(networkObject, networkId, sceneObject, playerObject, ownerClientId, destroyWithScene);
}

Expand Down Expand Up @@ -820,6 +851,12 @@ internal void OnDespawnObject(NetworkObject networkObject, bool destroyGameObjec
// and only attempt to remove the child's parent on the server-side
if (!NetworkManager.ShutdownInProgress && NetworkManager.IsServer)
{
// Destroy GameObjects that depend on the despawned GameObject
foreach (var dependingNetworkObject in networkObject.DependingNetworkObjects)
{
dependingNetworkObject.Despawn();
}

// Move child NetworkObjects to the root when parent NetworkObject is destroyed
foreach (var spawnedNetObj in SpawnedObjectsList)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -485,6 +485,20 @@ public static void RegisterNetcodeIntegrationTest(bool registered)
}


private static void TryAddObjectNameIdentifier(NetworkObject networkObject)
{
// To avoid issues with integration tests that forget to clean up,
// this feature only works with NetcodeIntegrationTest derived classes
if (IsNetcodeIntegrationTestRunning)
{
if (networkObject.GetComponent<ObjectNameIdentifier>() == null && networkObject.GetComponentInChildren<ObjectNameIdentifier>() == null)
{
// Add the object identifier component
networkObject.gameObject.AddComponent<ObjectNameIdentifier>();
}
}
}

/// <summary>
/// Normally we would only allow player prefabs to be set to a prefab. Not runtime created objects.
/// In order to prevent having a Resource folder full of a TON of prefabs that we have to maintain,
Expand All @@ -511,16 +525,7 @@ public static void MakeNetworkObjectTestPrefab(NetworkObject networkObject, uint
// 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)
{
if (networkObject.GetComponent<ObjectNameIdentifier>() == null && networkObject.GetComponentInChildren<ObjectNameIdentifier>() == null)
{
// Add the object identifier component
networkObject.gameObject.AddComponent<ObjectNameIdentifier>();
}
}
TryAddObjectNameIdentifier(networkObject);
}

public static GameObject CreateNetworkObjectPrefab(string baseName, NetworkManager server, params NetworkManager[] clients)
Expand Down Expand Up @@ -553,6 +558,27 @@ void AddNetworkPrefab(NetworkConfig config, NetworkPrefab prefab)
return gameObject;
}

public static GameObject AddNetworkObjectChildToPrefab(NetworkObject prefab, string baseName)
{
var gameObject = new GameObject
{
name = baseName
};
gameObject.transform.parent = prefab.transform;
var networkObject = gameObject.AddComponent<NetworkObject>();


var currentDependingNetworkObject = prefab.DependingNetworkObjects;
currentDependingNetworkObject.Add(networkObject);
prefab.DependingNetworkObjects = currentDependingNetworkObject;

networkObject.DependentNetworkObject = prefab;
networkObject.IsSceneObject = false;

TryAddObjectNameIdentifier(networkObject);
return gameObject;
}

// We use GameObject instead of SceneObject to be able to keep hierarchy
public static void MarkAsSceneObjectRoot(GameObject networkObjectRoot, NetworkManager server, NetworkManager[] clients)
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
using System.Collections;
using System.Collections.Generic;
using NUnit.Framework;
using Unity.Netcode.TestHelpers.Runtime;
using UnityEngine;
using UnityEngine.TestTools;
using Object = UnityEngine.Object;

namespace Unity.Netcode.RuntimeTests
{
/// <summary>
/// Tests ensuring that dependent <see cref="NetworkObject"/>s are functioning properly. Expected behavior:
/// -
/// </summary>
public class NetworkObjectDependencyTests : NetcodeIntegrationTest
{
protected override int NumberOfClients => 1;

private GameObject m_PrefabToSpawn;

protected override void OnCreatePlayerPrefab()
{
NetworkObject playerNetworkObject = m_PlayerPrefab.GetComponent<NetworkObject>();
GameObject childObject = NetcodeIntegrationTestHelpers.AddNetworkObjectChildToPrefab(playerNetworkObject, "child");
}

protected override void OnServerAndClientsCreated()
{
m_PrefabToSpawn = CreateNetworkObjectPrefab("PrefabWithChildNetworkObject");
NetcodeIntegrationTestHelpers.AddNetworkObjectChildToPrefab(m_PrefabToSpawn.GetComponent<NetworkObject>(), "child");
}

protected override void OnNewClientCreated(NetworkManager networkManager)
{
var networkPrefab = new NetworkPrefab() { Prefab = m_PrefabToSpawn };
networkManager.NetworkConfig.Prefabs.Add(networkPrefab);
}


/// <summary>
/// Tests that depending <see cref="NetworkObject"/> on a player objects will be synchronized.
/// </summary>
[UnityTest]
public IEnumerator TestPlayerDependingObjects()
{
// This is the *SERVER VERSION* of the *CLIENT PLAYER*
var serverClientPlayerResult = new NetcodeIntegrationTestHelpers.ResultWrapper<NetworkObject>();
yield return NetcodeIntegrationTestHelpers.GetNetworkObjectByRepresentation(x => x.IsPlayerObject && x.OwnerClientId == m_ClientNetworkManagers[0].LocalClientId, m_ServerNetworkManager, serverClientPlayerResult);

// This is the *CLIENT VERSION* of the *CLIENT PLAYER*
var clientClientPlayerResult = new NetcodeIntegrationTestHelpers.ResultWrapper<NetworkObject>();
yield return NetcodeIntegrationTestHelpers.GetNetworkObjectByRepresentation(x => x.IsPlayerObject && x.OwnerClientId == m_ClientNetworkManagers[0].LocalClientId, m_ClientNetworkManagers[0], clientClientPlayerResult);

Assert.IsNotNull(serverClientPlayerResult.Result.gameObject);
Assert.IsNotNull(clientClientPlayerResult.Result.gameObject);

var serverClientPlayerChild = serverClientPlayerResult.Result.transform.GetChild(0)?.GetComponent<NetworkObject>();
var clientClientPlayerChild = clientClientPlayerResult.Result.transform.GetChild(0)?.GetComponent<NetworkObject>();

Assert.IsNotNull(serverClientPlayerChild);
Assert.IsNotNull(clientClientPlayerChild);

Assert.IsTrue(serverClientPlayerChild.NetworkObjectId == clientClientPlayerChild.NetworkObjectId); // They should have the same NetworkObjectId
Assert.IsTrue(serverClientPlayerChild.NetworkObjectId > default(ulong)); // and that id should have been set
}

/// <summary>
/// Tests that depending <see cref="NetworkObject"/>s can be reparented
/// and that the reparenting will be synchronized to late-joining clients.
/// </summary>
[UnityTest]
public IEnumerator TestDependingObjectReparenting()
{
var serverDependingInstance = SpawnObject(m_PrefabToSpawn, m_ServerNetworkManager).transform.GetChild(0)?.GetComponent<NetworkObject>();
Assert.IsNotNull(serverDependingInstance); // Sanity check

yield return NetcodeIntegrationTestHelpers.WaitForMessageOfTypeHandled<CreateObjectMessage>(m_ClientNetworkManagers[0]);

serverDependingInstance.transform.parent = null;

yield return NetcodeIntegrationTestHelpers.WaitForMessageOfTypeHandled<ParentSyncMessage>(m_ClientNetworkManagers[0]);

var clientDepending1Instance = s_GlobalNetworkObjects[m_ClientNetworkManagers[0].LocalClientId][serverDependingInstance.NetworkObjectId];
Assert.IsNull(clientDepending1Instance.transform.parent); // Make sure the client instance was reparented

yield return CreateAndStartNewClient();

var clientDepending2Instance = s_GlobalNetworkObjects[m_ClientNetworkManagers[1].LocalClientId][serverDependingInstance.NetworkObjectId];
Assert.IsNull(clientDepending2Instance.transform.parent); // Make sure the late-joining client instance was reparented
}

/// <summary>
/// Tests that depending <see cref="NetworkObject"/>s can be deleted,
/// and that those deletions will be synchronized across both connected
/// and late-joining clients.
/// </summary>
[UnityTest]
public IEnumerator TestDependingObjectDeletion()
{
var serverDependentInstance = SpawnObject(m_PrefabToSpawn, m_ServerNetworkManager).GetComponent<NetworkObject>();
var serverDependingInstance = serverDependentInstance.transform.GetChild(0)?.GetComponent<NetworkObject>();
Assert.IsNotNull(serverDependingInstance); // Sanity check

yield return NetcodeIntegrationTestHelpers.WaitForMessageOfTypeHandled<CreateObjectMessage>(m_ClientNetworkManagers[0]);

var clientDepending1Instance = s_GlobalNetworkObjects[m_ClientNetworkManagers[0].LocalClientId][serverDependingInstance.NetworkObjectId];
Object.Destroy(serverDependingInstance.gameObject);

yield return NetcodeIntegrationTestHelpers.WaitForMessageOfTypeHandled<DestroyObjectMessage>(m_ClientNetworkManagers[0]);

Assert.IsTrue(clientDepending1Instance == null, "Dependent NetworkObject was not destroyed on connected client.");

yield return CreateAndStartNewClient();

Assert.IsTrue(
!s_GlobalNetworkObjects[m_ClientNetworkManagers[1].LocalClientId].ContainsKey(serverDependingInstance.NetworkObjectId) ||
s_GlobalNetworkObjects[m_ClientNetworkManagers[1].LocalClientId][serverDependingInstance.NetworkObjectId] == null,
"Dependent NetworkObject was not destroyed on late-joining client.");
}

/// <summary>
/// Tests that deleting <see cref="NetworkObject"/>s also deletes any
/// <see cref="NetworkObject"/>s that are dependent on the deleted one.
/// </summary>
[UnityTest]
public IEnumerator TestDependentObjectDeletion()
{
var serverDependentInstance = SpawnObject(m_PrefabToSpawn, m_ServerNetworkManager).GetComponent<NetworkObject>();
Assert.IsTrue(serverDependentInstance.DependingNetworkObjects.Count > 0); // Make sure the prefab has a dependent NetworkObject
var serverDependingInstance = serverDependentInstance.DependingNetworkObjects[0];

yield return NetcodeIntegrationTestHelpers.WaitForMessageOfTypeHandled<CreateObjectMessage>(m_ClientNetworkManagers[0]);

var clientDependentInstance = s_GlobalNetworkObjects[m_ClientNetworkManagers[0].LocalClientId][serverDependentInstance.NetworkObjectId];
var clientDependingInstance = clientDependentInstance.DependingNetworkObjects[0];
Object.Destroy(serverDependentInstance.gameObject);

yield return NetcodeIntegrationTestHelpers.WaitForMessageOfTypeHandled<DestroyObjectMessage>(m_ClientNetworkManagers[0]); // Wait for parent deleting

Assert.IsTrue(serverDependentInstance == null, "Dependent NetworkObject was not destroyed on host.");
Assert.IsTrue(serverDependingInstance == null, "Depending NetworkObject was not destroyed on host.");
Assert.IsTrue(clientDependentInstance == null, "Dependent NetworkObject was not destroyed on connected client.");
Assert.IsTrue(clientDependingInstance == null, "Depending NetworkObject was not destroyed on connected client.");
}
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.