From 8d3616d36810f67ad3a9f9d91c31144c23971607 Mon Sep 17 00:00:00 2001 From: Noel Stephens Date: Fri, 30 Jul 2021 13:45:19 -0500 Subject: [PATCH 1/5] refactor and updates This updates the object pooling example to reflect the changes from PR-977: https://github.com/Unity-Technologies/com.unity.multiplayer.mlapi/pull/977 --- docs/advanced-topics/object-pooling.md | 250 +++++++++++++++++++++---- 1 file changed, 211 insertions(+), 39 deletions(-) diff --git a/docs/advanced-topics/object-pooling.md b/docs/advanced-topics/object-pooling.md index dcb117442..f64c0f8d8 100644 --- a/docs/advanced-topics/object-pooling.md +++ b/docs/advanced-topics/object-pooling.md @@ -5,7 +5,7 @@ title: Object Pooling The MLAPI provides built-in support for Object Pooling, which allows you to override the default MLAPI destroy and spawn handlers with your own logic. -This allows you to store destroyed network objects in a pool to reuse later. This is useful for frequently used objects, such as bullets, and can be used to increase the application's overall performance. +This allows you to store destroyed network objects in a pool to reuse later. This is useful for frequently used objects, such as projectiles, and is a way to increase the application's overall performance by decreasing the amount of objects being created over time. See [Introduction to Object Pooling](https://learn.unity.com/tutorial/introduction-to-object-pooling) to learn more about the importance of pooling objects. @@ -15,11 +15,13 @@ You can register your own spawn handlers by including the `INetworkPrefabInstanc ```csharp public interface INetworkPrefabInstanceHandler { - NetworkObject HandleNetworkPrefabSpawn(ulong ownerClientId, Vector3 position, Quaternion rotation); - void HandleNetworkPrefabDestroy(NetworkObject networkObject); + NetworkObject Instantiate(ulong ownerClientId, Vector3 position, Quaternion rotation); + void Destroy(NetworkObject networkObject); } ``` -MLAPI will use the `HandleNetworkPrefabSpawn` and `HandleNetworkPrefabDestroy` methods in place of default spawn handlers for the `NetworkObject` used during the registration process. In the following implementation example, the `m_OriginalPrefab` property is the prefab we will replace with the `m_TargetPrefabToSpawn`. As such, we register the `CustomPrefabHandlerExample` class (that implements the `INetworkPrefabInstanceHandler` interface) using the `m_OriginalPrefab`'s `NetworkObject` with a reference to the current instance of `CustomPrefabHandlerExample`. +MLAPI will use the `Instantiate` and `Destroy` methods in place of default spawn handlers for the `NetworkObject` used during spawning and despawning. Because the message to instantiate a new `NetworkObject` originates from a Host or Server, both will not have the Instantiate method invoked. All clients (excluding a Host) will have the instantiate method invoked if the `INetworkPrefabInstanceHandler` implementation is registered with `NetworkPrefabHanlder` (`NetworkManager.PrefabHandler`) and a Host or Server spawns the registered/associated `NetworkObject`. + +In the following basic pooling example, the `m_ObjectToPool` property is the prefab we want to pool. We register the `NetworkPrefabHandlerObjectPool` class (that implements the `INetworkPrefabInstanceHandler` interface) using the `m_ObjectToPool`'s `GameObject` with a reference to the current instance of `NetworkPrefabHandlerObjectPool`. We also take into account any `NetworkManager` defined `NetworkPreab` overrides by calling `NetworkManager.GetNetworkPrefabOverride` while both assigning and passing in our `m_ObjectToPool`. ```csharp using System.Collections.Generic; @@ -27,73 +29,66 @@ using UnityEngine; using MLAPI; using MLAPI.Spawning; -public class CustomPrefabHandlerExample : NetworkBehaviour, INetworkPrefabInstanceHandler +public class NetworkPrefabHandlerObjectPool : NetworkBehaviour, INetworkPrefabInstanceHandler { [SerializeField] - private GameObject m_OriginalPrefab; - - [SerializeField] - private GameObject m_TargetPrefabToSpawn; + private GameObject m_ObjectToPool; [SerializeField] private int m_ObjectPoolSize = 15; - private List m_NetworkObjectsPool; + private List m_ObjectsPool; - private void Start() + public override void OnNetworkSpawn() { if (NetworkManager && NetworkManager.PrefabHandler != null) { - NetworkManager.PrefabHandler.AddHandler(m_OriginalPrefab.GetComponent(), this); + NetworkManager.PrefabHandler.AddHandler(m_ObjectToPool, this); } - } - public override void NetworkStart() - { - if (m_OriginalPrefab != null && m_TargetPrefabToSpawn != null) + // Makes sure we have the right prefab + m_ObjectToPool = NetworkManager.GetNetworkPrefabOverride(m_ObjectToPool); + if (m_ObjectToPool != null) { - m_NetworkObjectsPool = new List(); + m_ObjectsPool = new List(); for (int i = 0; i < m_ObjectPoolSize; i++) { - InstantiateNewNetworkObject(); + InstantiatePoolObject().SetActive(false); } } } - private NetworkObject InstantiateNewNetworkObject() + private GameObject InstantiatePoolObject() { - var gameObject = Instantiate(m_TargetPrefabToSpawn); - var networkObject = gameObject.GetComponent(); - gameObject.SetActive(false); - m_NetworkObjectsPool.Add(networkObject); - return networkObject; + m_ObjectsPool.Add(Instantiate(m_ObjectToPool)); + return m_ObjectsPool[m_ObjectsPool.Count - 1]; } - private NetworkObject GetNextSpawnObject() + private GameObject GetNextSpawnObject() { - foreach (var networkObject in m_NetworkObjectsPool) + foreach (var gameObject in m_ObjectsPool) { - if (!networkObject.IsSpawned) + if (!gameObject.activeInHierarchy) { - return networkObject; + return gameObject; } } //We are out of objects, expand our pool by 1 more NetworkObject - return InstantiateNewNetworkObject(); + return InstantiatePoolObject(); } - public NetworkObject HandleNetworkPrefabSpawn(ulong ownerClientId, Vector3 position, Quaternion rotation) + public NetworkObject Instantiate(ulong ownerClientId, Vector3 position, Quaternion rotation) { - var networkObject = GetNextSpawnObject(); - networkObject.gameObject.SetActive(true); - networkObject.transform.position = position; - networkObject.transform.rotation = rotation; - return networkObject; + var gameObject = GetNextSpawnObject(); + gameObject.SetActive(true); + gameObject.transform.position = position; + gameObject.transform.rotation = rotation; + return gameObject.GetComponent(); } - public void HandleNetworkPrefabDestroy(NetworkObject networkObject) + public void Destroy(NetworkObject networkObject) { - if (m_NetworkObjectsPool.Contains(networkObject)) + if (m_ObjectsPool.Contains(networkObject.gameObject)) { networkObject.gameObject.SetActive(false); } @@ -101,6 +96,183 @@ public class CustomPrefabHandlerExample : NetworkBehaviour, INetworkPrefabInstan } ``` -Registering your own spawn handlers allows you to pool all networked objects on clients as they are destroyed and spawned on your clients. -To pool objects on the server side, do not use `Destroy`. Use `NetworkObject.Despawn` first, then manually pool the object. +In the next more advanced example, the `m_ObjectToOverride` property is the prefab we will replace with one of the `m_ObjectOverrides` prefabs. As such, we register the `CustomPrefabHandlerObjectPoolOverride` class (that implements the `INetworkPrefabInstanceHandler` interface) using the `m_ObjectToOverride` with a reference to the current instance of `CustomPrefabHandlerObjectPoolOverride`. We then have to handle a special case scenario. Since a Host is actually both a client and a server, we need to pre-register the link (association) between the `m_ObjectToOverride` prefab and the `m_ObjectOverrides` prefabs. We do this by calling `NetworkManager.PrefabHandler.RegisterHostGlobalObjectIdHashValues` and passing in the `m_ObjectToOverride` and the `m_ObjectOverrides` list. For both the Client and Host, we will create a pool for each prefab type in the m_ObjectOverrides list. If we are just a server (i.e. not a Host), then we only need to create a large pool containing only one prefab type: `m_ObjectToOverride`. We included this example in order to show that the common link between all instances is the `m_ObjectToOverride`'s GlobalObjectIdHash value. The `m_ObjectToOverride`'s GlobalObjectIdHash value is always used to signal the creation or destruction for all messages pertaining this prefab handler override. + +```csharp +using System.Collections; +using System.Collections.Generic; +using UnityEngine; +using MLAPI; +using MLAPI.Spawning; +using TestProject.ManualTests; + +public class CustomPrefabHandlerObjectPoolOverride : NetworkBehaviour, INetworkPrefabInstanceHandler +{ + private GameObject m_ObjectToPool; + + [SerializeField] + private GameObject m_ObjectToOverride; + + [SerializeField] + private List m_ObjectOverrides; + + [SerializeField] + private int m_ObjectPoolSize = 15; + + [SerializeField] + [Range(1, 5)] + private int m_SpawnsPerSecond = 2; + + private Dictionary> m_ObjectsPool; + private List m_NameValidation; + + private bool m_IsSpawningObjects; + + public override void OnNetworkSpawn() + { + // Register your object to be overridden (m_ObjectToOverride) with this INetworkPrefabInstanceHandler implementation + if (NetworkManager && NetworkManager.PrefabHandler != null) + { + NetworkManager.PrefabHandler.AddHandler(m_ObjectToOverride, this); + } + + // Start with the base object to be overridden (i.e. Server mode will always use this) + m_ObjectToPool = m_ObjectToOverride; + + // Host and Client need to do an extra step + if (IsClient) + { + // Makes sure we have the right prefab to create a pool for (i.e. Clients and Hosts) + m_ObjectToPool = NetworkManager.GetNetworkPrefabOverride(m_ObjectToPool); + + // Host Only: + // Since the host will be spawning overrides, we need to manually create the link between the + // m_ObjectToOverride and the objects that could override it (i.e. m_ObjectOverrides) + if (IsHost) + { + // While this seems redundant, we could theoretically have several objects that we could potentially be spawning + NetworkManager.PrefabHandler.RegisterHostGlobalObjectIdHashValues(m_ObjectToOverride, m_ObjectOverrides); + } + } + + m_ObjectsPool = new Dictionary>(); + m_NameValidation = new List(); + for (int x = 0; x < m_ObjectOverrides.Count; x++) + { + // If we are a server, then we just create a big pool of the same base override object + // otherwise for Host and Client we use the list of object overrides + var objectIndex = IsServer && !IsHost ? 0 : x; + var objectToPool = IsServer && !IsHost ? m_ObjectToOverride : m_ObjectOverrides[x]; + + if (!m_ObjectsPool.ContainsKey(objectIndex)) + { + m_ObjectsPool.Add(objectIndex, new List()); + } + + for (int y = 0; y < m_ObjectPoolSize; y++) + { + var newObject = Instantiate(objectToPool); + + // One way to verify this object exists + // You could also make this a dictionary that linked to the actual GameObject instance + newObject.name += m_ObjectsPool[objectIndex].Count.ToString(); + m_NameValidation.Add(newObject.name); + + // Make sure we start this object as inactive + newObject.SetActive(false); + m_ObjectsPool[objectIndex].Add(newObject); + } + } + + // Host and Server spawn the objects + if (IsServer) + { + StartCoroutine(SpawnObjects()); + } + } + + private GameObject GetNextSpawnObject() + { + // If we are just a server use index 0, otherwise we are a host or client so get a random override object to spawn + var indexType = IsServer && !IsHost ? 0 : Random.Range(0, m_ObjectOverrides.Count - 1); + + if (m_ObjectsPool.ContainsKey(indexType)) + { + foreach (var gameObject in m_ObjectsPool[indexType]) + { + if (!gameObject.activeInHierarchy) + { + return gameObject; + } + } + //We are out of objects, expand our pool by 1 more NetworkObject + var newObject = Instantiate(m_ObjectOverrides[indexType]); + m_ObjectsPool[indexType].Add(newObject); + return newObject; + } + // If requesting a bad index return null + return null; + } + + public NetworkObject Instantiate(ulong ownerClientId, Vector3 position, Quaternion rotation) + { + var gameObject = GetNextSpawnObject(); + gameObject.SetActive(true); + gameObject.transform.position = position; + gameObject.transform.rotation = rotation; + return gameObject.GetComponent(); + } + + public void Destroy(NetworkObject networkObject) + { + if (m_NameValidation.Contains(networkObject.gameObject.name)) + { + networkObject.gameObject.SetActive(false); + } + } + + /// + /// Spawns the objects. + /// Note: You can find the GenericNetworkObjectBehaviour here: + /// https://github.com/Unity-Technologies/com.unity.multiplayer.mlapi/blob/develop/testproject/Assets/Tests/Manual/Scripts/GenericNetworkObjectBehaviour.cs + /// + /// IEnumerator + private IEnumerator SpawnObjects() + { + //Exit if we are a client or we happen to not have a NetworkManager + if (NetworkManager == null || (NetworkManager.IsClient && !NetworkManager.IsHost && !NetworkManager.IsServer)) + { + yield return null; + } + + m_IsSpawningObjects = true; + + var entitySpawnUpdateRate = 1.0f; + while (m_IsSpawningObjects) + { + entitySpawnUpdateRate = 1.0f / (float)m_SpawnsPerSecond; + + GameObject go = GetNextSpawnObject(); + if (go != null) + { + go.SetActive(true); + go.transform.position = transform.position; + + float ang = Random.Range(0.0f, 2 * Mathf.PI); + go.GetComponent().SetDirectionAndVelocity(new Vector3(Mathf.Cos(ang), 0, Mathf.Sin(ang)), 4); + + var no = go.GetComponent(); + if (!no.IsSpawned) + { + no.Spawn(null, true); + } + } + yield return new WaitForSeconds(entitySpawnUpdateRate); + } + } +} +``` + +Using `INetworkPrefabInstanceHandler` implementations simplifies object pooling while also providing the ability to have different versions for the same NetworkObject instance as it is viewed by Clients (including the Host). Additionally, you do not need to register the network prefabs assigned to the `m_ObjectOverrides` list with the NetworkManager since each local overriding prefab instance is linked by the `NetworkObject.NetworkObjectId`. However, you **do** need to register the prefab to be overridden (i.e. `m_ObjectToOverride` in the above example). +While this provides many possibilities, you must take caution when using multiple prefabs as overrides by making sure that every variation has the same associated `NetworkVariables` and `RPC` implementations as all variations **must be identical** in this regard when it comes to anything that could be communicated between the client(s) and server. Otherwise, you could end up with messages being sent to override instances that don't know how to handle them! From 81d0b49e19ec5eef564447ee18b0e1b483524e71 Mon Sep 17 00:00:00 2001 From: Noel Stephens Date: Sun, 1 Aug 2021 16:48:46 -0500 Subject: [PATCH 2/5] additional notes This adds additional information about using more than one prefab with an override. This also adds a link to the to-be-existing folder in the master branch. --- docs/advanced-topics/object-pooling.md | 132 +++++++++++++++++++++---- 1 file changed, 112 insertions(+), 20 deletions(-) diff --git a/docs/advanced-topics/object-pooling.md b/docs/advanced-topics/object-pooling.md index f64c0f8d8..42ea6c0d3 100644 --- a/docs/advanced-topics/object-pooling.md +++ b/docs/advanced-topics/object-pooling.md @@ -3,9 +3,7 @@ id: object-pooling title: Object Pooling --- -The MLAPI provides built-in support for Object Pooling, which allows you to override the default MLAPI destroy and spawn handlers with your own logic. - -This allows you to store destroyed network objects in a pool to reuse later. This is useful for frequently used objects, such as projectiles, and is a way to increase the application's overall performance by decreasing the amount of objects being created over time. +The MLAPI provides built-in support for Object Pooling, which allows you to override the default MLAPI destroy and spawn handlers with your own logic. This allows you to store destroyed network objects in a pool to reuse later. This is useful for frequently used objects, such as projectiles, and is a way to increase the application's overall performance by decreasing the amount of objects being created over time. See [Introduction to Object Pooling](https://learn.unity.com/tutorial/introduction-to-object-pooling) to learn more about the importance of pooling objects. @@ -22,8 +20,8 @@ You can register your own spawn handlers by including the `INetworkPrefabInstanc MLAPI will use the `Instantiate` and `Destroy` methods in place of default spawn handlers for the `NetworkObject` used during spawning and despawning. Because the message to instantiate a new `NetworkObject` originates from a Host or Server, both will not have the Instantiate method invoked. All clients (excluding a Host) will have the instantiate method invoked if the `INetworkPrefabInstanceHandler` implementation is registered with `NetworkPrefabHanlder` (`NetworkManager.PrefabHandler`) and a Host or Server spawns the registered/associated `NetworkObject`. In the following basic pooling example, the `m_ObjectToPool` property is the prefab we want to pool. We register the `NetworkPrefabHandlerObjectPool` class (that implements the `INetworkPrefabInstanceHandler` interface) using the `m_ObjectToPool`'s `GameObject` with a reference to the current instance of `NetworkPrefabHandlerObjectPool`. We also take into account any `NetworkManager` defined `NetworkPreab` overrides by calling `NetworkManager.GetNetworkPrefabOverride` while both assigning and passing in our `m_ObjectToPool`. - ```csharp +using System.Collections; using System.Collections.Generic; using UnityEngine; using MLAPI; @@ -37,8 +35,14 @@ public class NetworkPrefabHandlerObjectPool : NetworkBehaviour, INetworkPrefabIn [SerializeField] private int m_ObjectPoolSize = 15; + [SerializeField] + [Range(1, 5)] + private int m_SpawnsPerSecond = 2; + private List m_ObjectsPool; + private bool m_IsSpawningObjects; + public override void OnNetworkSpawn() { if (NetworkManager && NetworkManager.PrefabHandler != null) @@ -46,8 +50,12 @@ public class NetworkPrefabHandlerObjectPool : NetworkBehaviour, INetworkPrefabIn NetworkManager.PrefabHandler.AddHandler(m_ObjectToPool, this); } - // Makes sure we have the right prefab - m_ObjectToPool = NetworkManager.GetNetworkPrefabOverride(m_ObjectToPool); + // This assures we have the right prefab + if (IsClient) + { + m_ObjectToPool = NetworkManager.GetNetworkPrefabOverride(m_ObjectToPool); + } + if (m_ObjectToPool != null) { m_ObjectsPool = new List(); @@ -56,6 +64,12 @@ public class NetworkPrefabHandlerObjectPool : NetworkBehaviour, INetworkPrefabIn InstantiatePoolObject().SetActive(false); } } + + // Host and Server spawn the objects + if (IsServer) + { + StartCoroutine(SpawnObjects()); + } } private GameObject InstantiatePoolObject() @@ -93,10 +107,43 @@ public class NetworkPrefabHandlerObjectPool : NetworkBehaviour, INetworkPrefabIn networkObject.gameObject.SetActive(false); } } + + private IEnumerator SpawnObjects() + { + //Exit if we are a client or we happen to not have a NetworkManager + if (NetworkManager == null || (NetworkManager.IsClient && !NetworkManager.IsHost && !NetworkManager.IsServer)) + { + yield return null; + } + + m_IsSpawningObjects = true; + + var entitySpawnUpdateRate = 1.0f; + while (m_IsSpawningObjects) + { + entitySpawnUpdateRate = 1.0f / (float)m_SpawnsPerSecond; + + GameObject go = GetNextSpawnObject(); + if (go != null) + { + go.SetActive(true); + go.transform.position = transform.position; + + float ang = Random.Range(0.0f, 2 * Mathf.PI); + go.GetComponent().SetDirectionAndVelocity(new Vector3(Mathf.Cos(ang), 0, Mathf.Sin(ang)), 4); + + var no = go.GetComponent(); + if (!no.IsSpawned) + { + no.Spawn(null, true); + } + } + yield return new WaitForSeconds(entitySpawnUpdateRate); + } + } } ``` - In the next more advanced example, the `m_ObjectToOverride` property is the prefab we will replace with one of the `m_ObjectOverrides` prefabs. As such, we register the `CustomPrefabHandlerObjectPoolOverride` class (that implements the `INetworkPrefabInstanceHandler` interface) using the `m_ObjectToOverride` with a reference to the current instance of `CustomPrefabHandlerObjectPoolOverride`. We then have to handle a special case scenario. Since a Host is actually both a client and a server, we need to pre-register the link (association) between the `m_ObjectToOverride` prefab and the `m_ObjectOverrides` prefabs. We do this by calling `NetworkManager.PrefabHandler.RegisterHostGlobalObjectIdHashValues` and passing in the `m_ObjectToOverride` and the `m_ObjectOverrides` list. For both the Client and Host, we will create a pool for each prefab type in the m_ObjectOverrides list. If we are just a server (i.e. not a Host), then we only need to create a large pool containing only one prefab type: `m_ObjectToOverride`. We included this example in order to show that the common link between all instances is the `m_ObjectToOverride`'s GlobalObjectIdHash value. The `m_ObjectToOverride`'s GlobalObjectIdHash value is always used to signal the creation or destruction for all messages pertaining this prefab handler override. ```csharp @@ -105,9 +152,10 @@ using System.Collections.Generic; using UnityEngine; using MLAPI; using MLAPI.Spawning; -using TestProject.ManualTests; +using MLAPI.Serialization; +using MLAPI.Serialization.Pooled; -public class CustomPrefabHandlerObjectPoolOverride : NetworkBehaviour, INetworkPrefabInstanceHandler +public class NetworkPrefabHandlerObjectPoolOverride : NetworkBehaviour, INetworkPrefabInstanceHandler { private GameObject m_ObjectToPool; @@ -120,6 +168,9 @@ public class CustomPrefabHandlerObjectPoolOverride : NetworkBehaviour, INetworkP [SerializeField] private int m_ObjectPoolSize = 15; + [SerializeField] + private bool m_SynchronizeOverrides; + [SerializeField] [Range(1, 5)] private int m_SpawnsPerSecond = 2; @@ -162,8 +213,8 @@ public class CustomPrefabHandlerObjectPoolOverride : NetworkBehaviour, INetworkP { // If we are a server, then we just create a big pool of the same base override object // otherwise for Host and Client we use the list of object overrides - var objectIndex = IsServer && !IsHost ? 0 : x; - var objectToPool = IsServer && !IsHost ? m_ObjectToOverride : m_ObjectOverrides[x]; + var objectIndex = (IsServer && !IsHost) ? 0 : x; + var objectToPool = (IsServer && !IsHost) ? m_ObjectToOverride : m_ObjectOverrides[objectIndex]; if (!m_ObjectsPool.ContainsKey(objectIndex)) { @@ -192,7 +243,7 @@ public class CustomPrefabHandlerObjectPoolOverride : NetworkBehaviour, INetworkP } } - private GameObject GetNextSpawnObject() + private GameObject GetNextSpawnObject(int synchronizedIndex = -1) { // If we are just a server use index 0, otherwise we are a host or client so get a random override object to spawn var indexType = IsServer && !IsHost ? 0 : Random.Range(0, m_ObjectOverrides.Count - 1); @@ -206,8 +257,13 @@ public class CustomPrefabHandlerObjectPoolOverride : NetworkBehaviour, INetworkP return gameObject; } } - //We are out of objects, expand our pool by 1 more NetworkObject - var newObject = Instantiate(m_ObjectOverrides[indexType]); + // We are out of objects, get the type of object we need to instantiate and add to the pool + var objectToPool = (IsServer && !IsHost) ? m_ObjectToOverride : m_ObjectOverrides[indexType]; + + // Expand our pool by 1 more NetworkObject + var newObject = Instantiate(objectToPool); + var genericObjectPooledBehaviour = NetworkObject.GetComponent(); + genericObjectPooledBehaviour.SyncrhonizedObjectTypeIndex = (IsServer && !IsHost) ? Random.Range(0, m_ObjectOverrides.Count - 1) : indexType; m_ObjectsPool[indexType].Add(newObject); return newObject; } @@ -215,6 +271,18 @@ public class CustomPrefabHandlerObjectPoolOverride : NetworkBehaviour, INetworkP return null; } + public void OnSynchronizeWrite(NetworkWriter networkWriter, NetworkObject networkObject) + { + var genericObjectPooledBehaviour = NetworkObject.GetComponent(); + networkWriter.WriteInt32Packed(genericObjectPooledBehaviour.SyncrhonizedObjectTypeIndex); + } + + private int m_UseSynchronizedIndexValue = -1; + public void OnSynchronizeRead(NetworkReader networkReader) + { + m_UseSynchronizedIndexValue = networkReader.ReadInt32Packed(); + } + public NetworkObject Instantiate(ulong ownerClientId, Vector3 position, Quaternion rotation) { var gameObject = GetNextSpawnObject(); @@ -234,8 +302,6 @@ public class CustomPrefabHandlerObjectPoolOverride : NetworkBehaviour, INetworkP /// /// Spawns the objects. - /// Note: You can find the GenericNetworkObjectBehaviour here: - /// https://github.com/Unity-Technologies/com.unity.multiplayer.mlapi/blob/develop/testproject/Assets/Tests/Manual/Scripts/GenericNetworkObjectBehaviour.cs /// /// IEnumerator private IEnumerator SpawnObjects() @@ -249,6 +315,16 @@ public class CustomPrefabHandlerObjectPoolOverride : NetworkBehaviour, INetworkP m_IsSpawningObjects = true; var entitySpawnUpdateRate = 1.0f; + + var networkBuffer = (NetworkBuffer)null; + var networkWriter = (PooledNetworkWriter)null; + + if(m_SynchronizeOverrides) + { + networkBuffer = new NetworkBuffer(2); + networkWriter = PooledNetworkWriter.Get(networkBuffer); + } + while (m_IsSpawningObjects) { entitySpawnUpdateRate = 1.0f / (float)m_SpawnsPerSecond; @@ -260,19 +336,35 @@ public class CustomPrefabHandlerObjectPoolOverride : NetworkBehaviour, INetworkP go.transform.position = transform.position; float ang = Random.Range(0.0f, 2 * Mathf.PI); - go.GetComponent().SetDirectionAndVelocity(new Vector3(Mathf.Cos(ang), 0, Mathf.Sin(ang)), 4); + go.GetComponent().SetDirectionAndVelocity(new Vector3(Mathf.Cos(ang), 0, Mathf.Sin(ang)), 4); var no = go.GetComponent(); if (!no.IsSpawned) { - no.Spawn(null, true); + if (m_SynchronizeOverrides) + { + networkBuffer.Position = 0; + var genericObjectPooledBehaviour = no.GetComponent(); + networkWriter.WriteInt32Packed(genericObjectPooledBehaviour.SyncrhonizedObjectTypeIndex); + } + no.Spawn(networkBuffer, true); } } yield return new WaitForSeconds(entitySpawnUpdateRate); } + + if (m_SynchronizeOverrides) + { + NetworkWriterPool.PutBackInPool(networkWriter); + networkBuffer.Dispose(); + } } } ``` -Using `INetworkPrefabInstanceHandler` implementations simplifies object pooling while also providing the ability to have different versions for the same NetworkObject instance as it is viewed by Clients (including the Host). Additionally, you do not need to register the network prefabs assigned to the `m_ObjectOverrides` list with the NetworkManager since each local overriding prefab instance is linked by the `NetworkObject.NetworkObjectId`. However, you **do** need to register the prefab to be overridden (i.e. `m_ObjectToOverride` in the above example). -While this provides many possibilities, you must take caution when using multiple prefabs as overrides by making sure that every variation has the same associated `NetworkVariables` and `RPC` implementations as all variations **must be identical** in this regard when it comes to anything that could be communicated between the client(s) and server. Otherwise, you could end up with messages being sent to override instances that don't know how to handle them! +Using `INetworkPrefabInstanceHandler` implementations simplifies object pooling while also providing the ability to have different versions for the same NetworkObject instance as it is viewed by Clients (including the Host). Additionally, you do not need to register the network prefabs assigned to the `m_ObjectOverrides` list with the NetworkManager since each local overriding prefab instance is linked by the `NetworkObject.NetworkObjectId`. However, you **do** need to register the prefab to be overridden (i.e. `m_ObjectToOverride` in the above example). While this provides many possibilities, you must take caution when using multiple prefabs as overrides by making sure that every variation has the same associated `NetworkVariables` and `RPC` implementations as all variations **must be identical** in this regard when it comes to anything that could be communicated between the client(s) and server. Otherwise, you could end up with messages being sent to override instances that don't know how to handle them! + +When using more than one network prefab, it is important to understand that each client determines what prefab they will be using and will not be synchronized across other clients. This feature is primarily to be used for things like platform specific Network Prefabs where things like collision models or graphics related assets might need to vary between platforms. + +You can find full working versions of the above two examples in the [testproject/Assets/Samples/PrefabPool](https://github.com/Unity-Technologies/com.unity.multiplayer.mlapi/tree/master/testproject/Assets/Samples/PrefabPool) repository directory. + From 99fb21aec4251c751e68d1a7789c13fa4516a8b0 Mon Sep 17 00:00:00 2001 From: Noel Stephens Date: Mon, 2 Aug 2021 11:46:11 -0500 Subject: [PATCH 3/5] refactor Removed some code from advanced example that was not being used. --- docs/advanced-topics/object-pooling.md | 41 ++++++-------------------- 1 file changed, 9 insertions(+), 32 deletions(-) diff --git a/docs/advanced-topics/object-pooling.md b/docs/advanced-topics/object-pooling.md index 42ea6c0d3..7ae4939b2 100644 --- a/docs/advanced-topics/object-pooling.md +++ b/docs/advanced-topics/object-pooling.md @@ -153,8 +153,15 @@ using UnityEngine; using MLAPI; using MLAPI.Spawning; using MLAPI.Serialization; -using MLAPI.Serialization.Pooled; +/// +/// This is an example of using more than one Network Prefab override when using a custom handler +/// USAGE NOTE: When using more than one network prefab, it is important to understand that each +/// client determines what prefab they will be using and will not be synchronized across other clients. +/// This feature is primarily to be used for things like platform specific Network Prefabs where +/// things like collision models or graphics related assets might need to vary between platforms. +/// The usage of different visual assets used is strictly for example purposes only. +/// public class NetworkPrefabHandlerObjectPoolOverride : NetworkBehaviour, INetworkPrefabInstanceHandler { private GameObject m_ObjectToPool; @@ -168,9 +175,6 @@ public class NetworkPrefabHandlerObjectPoolOverride : NetworkBehaviour, INetwork [SerializeField] private int m_ObjectPoolSize = 15; - [SerializeField] - private bool m_SynchronizeOverrides; - [SerializeField] [Range(1, 5)] private int m_SpawnsPerSecond = 2; @@ -277,12 +281,6 @@ public class NetworkPrefabHandlerObjectPoolOverride : NetworkBehaviour, INetwork networkWriter.WriteInt32Packed(genericObjectPooledBehaviour.SyncrhonizedObjectTypeIndex); } - private int m_UseSynchronizedIndexValue = -1; - public void OnSynchronizeRead(NetworkReader networkReader) - { - m_UseSynchronizedIndexValue = networkReader.ReadInt32Packed(); - } - public NetworkObject Instantiate(ulong ownerClientId, Vector3 position, Quaternion rotation) { var gameObject = GetNextSpawnObject(); @@ -316,15 +314,6 @@ public class NetworkPrefabHandlerObjectPoolOverride : NetworkBehaviour, INetwork var entitySpawnUpdateRate = 1.0f; - var networkBuffer = (NetworkBuffer)null; - var networkWriter = (PooledNetworkWriter)null; - - if(m_SynchronizeOverrides) - { - networkBuffer = new NetworkBuffer(2); - networkWriter = PooledNetworkWriter.Get(networkBuffer); - } - while (m_IsSpawningObjects) { entitySpawnUpdateRate = 1.0f / (float)m_SpawnsPerSecond; @@ -341,23 +330,11 @@ public class NetworkPrefabHandlerObjectPoolOverride : NetworkBehaviour, INetwork var no = go.GetComponent(); if (!no.IsSpawned) { - if (m_SynchronizeOverrides) - { - networkBuffer.Position = 0; - var genericObjectPooledBehaviour = no.GetComponent(); - networkWriter.WriteInt32Packed(genericObjectPooledBehaviour.SyncrhonizedObjectTypeIndex); - } - no.Spawn(networkBuffer, true); + no.Spawn(null,true); } } yield return new WaitForSeconds(entitySpawnUpdateRate); } - - if (m_SynchronizeOverrides) - { - NetworkWriterPool.PutBackInPool(networkWriter); - networkBuffer.Dispose(); - } } } ``` From 197b410d49780560617c72a9b93406cbcfbfd88b Mon Sep 17 00:00:00 2001 From: Noel Stephens Date: Thu, 5 Aug 2021 16:04:02 -0500 Subject: [PATCH 4/5] refactor Updating to most recent changes from PR-955 (yet to be merged) --- docs/advanced-topics/object-pooling.md | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/docs/advanced-topics/object-pooling.md b/docs/advanced-topics/object-pooling.md index 7ae4939b2..76a37b1b0 100644 --- a/docs/advanced-topics/object-pooling.md +++ b/docs/advanced-topics/object-pooling.md @@ -14,7 +14,7 @@ You can register your own spawn handlers by including the `INetworkPrefabInstanc public interface INetworkPrefabInstanceHandler { NetworkObject Instantiate(ulong ownerClientId, Vector3 position, Quaternion rotation); - void Destroy(NetworkObject networkObject); + bool Destroy(NetworkObject networkObject); } ``` MLAPI will use the `Instantiate` and `Destroy` methods in place of default spawn handlers for the `NetworkObject` used during spawning and despawning. Because the message to instantiate a new `NetworkObject` originates from a Host or Server, both will not have the Instantiate method invoked. All clients (excluding a Host) will have the instantiate method invoked if the `INetworkPrefabInstanceHandler` implementation is registered with `NetworkPrefabHanlder` (`NetworkManager.PrefabHandler`) and a Host or Server spawns the registered/associated `NetworkObject`. @@ -24,8 +24,7 @@ In the following basic pooling example, the `m_ObjectToPool` property is the pre using System.Collections; using System.Collections.Generic; using UnityEngine; -using MLAPI; -using MLAPI.Spawning; +using Unity.Netcode; public class NetworkPrefabHandlerObjectPool : NetworkBehaviour, INetworkPrefabInstanceHandler { @@ -100,12 +99,13 @@ public class NetworkPrefabHandlerObjectPool : NetworkBehaviour, INetworkPrefabIn return gameObject.GetComponent(); } - public void Destroy(NetworkObject networkObject) + public bool Destroy(NetworkObject networkObject) { if (m_ObjectsPool.Contains(networkObject.gameObject)) { networkObject.gameObject.SetActive(false); } + return false; } private IEnumerator SpawnObjects() @@ -135,7 +135,7 @@ public class NetworkPrefabHandlerObjectPool : NetworkBehaviour, INetworkPrefabIn var no = go.GetComponent(); if (!no.IsSpawned) { - no.Spawn(null, true); + no.Spawn(true); } } yield return new WaitForSeconds(entitySpawnUpdateRate); @@ -150,9 +150,7 @@ In the next more advanced example, the `m_ObjectToOverride` property is the pref using System.Collections; using System.Collections.Generic; using UnityEngine; -using MLAPI; -using MLAPI.Spawning; -using MLAPI.Serialization; +using Unity.Netcode; /// /// This is an example of using more than one Network Prefab override when using a custom handler @@ -290,12 +288,14 @@ public class NetworkPrefabHandlerObjectPoolOverride : NetworkBehaviour, INetwork return gameObject.GetComponent(); } - public void Destroy(NetworkObject networkObject) + public bool Destroy(NetworkObject networkObject) { if (m_NameValidation.Contains(networkObject.gameObject.name)) { networkObject.gameObject.SetActive(false); } + + return false; } /// @@ -330,7 +330,7 @@ public class NetworkPrefabHandlerObjectPoolOverride : NetworkBehaviour, INetwork var no = go.GetComponent(); if (!no.IsSpawned) { - no.Spawn(null,true); + no.Spawn(true); } } yield return new WaitForSeconds(entitySpawnUpdateRate); From 31a0dc743ea52e8dc1b1832a002a5795b9de5119 Mon Sep 17 00:00:00 2001 From: Noel Stephens Date: Tue, 24 Aug 2021 15:49:34 -0500 Subject: [PATCH 5/5] refactor making changes to reflect the final version that was merged into the develop branch. --- docs/advanced-topics/object-pooling.md | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/docs/advanced-topics/object-pooling.md b/docs/advanced-topics/object-pooling.md index 76a37b1b0..a0c13eb54 100644 --- a/docs/advanced-topics/object-pooling.md +++ b/docs/advanced-topics/object-pooling.md @@ -14,7 +14,7 @@ You can register your own spawn handlers by including the `INetworkPrefabInstanc public interface INetworkPrefabInstanceHandler { NetworkObject Instantiate(ulong ownerClientId, Vector3 position, Quaternion rotation); - bool Destroy(NetworkObject networkObject); + void Destroy(NetworkObject networkObject); } ``` MLAPI will use the `Instantiate` and `Destroy` methods in place of default spawn handlers for the `NetworkObject` used during spawning and despawning. Because the message to instantiate a new `NetworkObject` originates from a Host or Server, both will not have the Instantiate method invoked. All clients (excluding a Host) will have the instantiate method invoked if the `INetworkPrefabInstanceHandler` implementation is registered with `NetworkPrefabHanlder` (`NetworkManager.PrefabHandler`) and a Host or Server spawns the registered/associated `NetworkObject`. @@ -99,13 +99,20 @@ public class NetworkPrefabHandlerObjectPool : NetworkBehaviour, INetworkPrefabIn return gameObject.GetComponent(); } - public bool Destroy(NetworkObject networkObject) + private void OnDisable() + { + if (NetworkManager && NetworkManager.PrefabHandler != null) + { + NetworkManager.PrefabHandler.RemoveHandler(m_ObjectToPool); + } + } + + public void Destroy(NetworkObject networkObject) { if (m_ObjectsPool.Contains(networkObject.gameObject)) { networkObject.gameObject.SetActive(false); } - return false; } private IEnumerator SpawnObjects() @@ -142,6 +149,7 @@ public class NetworkPrefabHandlerObjectPool : NetworkBehaviour, INetworkPrefabIn } } } + ``` In the next more advanced example, the `m_ObjectToOverride` property is the prefab we will replace with one of the `m_ObjectOverrides` prefabs. As such, we register the `CustomPrefabHandlerObjectPoolOverride` class (that implements the `INetworkPrefabInstanceHandler` interface) using the `m_ObjectToOverride` with a reference to the current instance of `CustomPrefabHandlerObjectPoolOverride`. We then have to handle a special case scenario. Since a Host is actually both a client and a server, we need to pre-register the link (association) between the `m_ObjectToOverride` prefab and the `m_ObjectOverrides` prefabs. We do this by calling `NetworkManager.PrefabHandler.RegisterHostGlobalObjectIdHashValues` and passing in the `m_ObjectToOverride` and the `m_ObjectOverrides` list. For both the Client and Host, we will create a pool for each prefab type in the m_ObjectOverrides list. If we are just a server (i.e. not a Host), then we only need to create a large pool containing only one prefab type: `m_ObjectToOverride`. We included this example in order to show that the common link between all instances is the `m_ObjectToOverride`'s GlobalObjectIdHash value. The `m_ObjectToOverride`'s GlobalObjectIdHash value is always used to signal the creation or destruction for all messages pertaining this prefab handler override. @@ -288,14 +296,12 @@ public class NetworkPrefabHandlerObjectPoolOverride : NetworkBehaviour, INetwork return gameObject.GetComponent(); } - public bool Destroy(NetworkObject networkObject) + public void Destroy(NetworkObject networkObject) { if (m_NameValidation.Contains(networkObject.gameObject.name)) { networkObject.gameObject.SetActive(false); } - - return false; } /// @@ -339,7 +345,7 @@ public class NetworkPrefabHandlerObjectPoolOverride : NetworkBehaviour, INetwork } ``` -Using `INetworkPrefabInstanceHandler` implementations simplifies object pooling while also providing the ability to have different versions for the same NetworkObject instance as it is viewed by Clients (including the Host). Additionally, you do not need to register the network prefabs assigned to the `m_ObjectOverrides` list with the NetworkManager since each local overriding prefab instance is linked by the `NetworkObject.NetworkObjectId`. However, you **do** need to register the prefab to be overridden (i.e. `m_ObjectToOverride` in the above example). While this provides many possibilities, you must take caution when using multiple prefabs as overrides by making sure that every variation has the same associated `NetworkVariables` and `RPC` implementations as all variations **must be identical** in this regard when it comes to anything that could be communicated between the client(s) and server. Otherwise, you could end up with messages being sent to override instances that don't know how to handle them! +Using `INetworkPrefabInstanceHandler` implementations simplifies object pooling while also providing the ability to have different versions for the same NetworkObject instance as it is viewed by Clients (including the Host). Additionally, you do not need to register the network prefabs assigned to the `m_ObjectOverrides` list with the NetworkManager since each local overriding prefab instance is linked by the `NetworkObject.NetworkObjectId`. However, you **do** need to register the prefab to be overridden (i.e. `m_ObjectToOverride` in the above example). While this provides many possibilities, you must take caution when using multiple prefabs as overrides by making sure that every variation has the same associated `NetworkVariables` and `RPC` implementations as all variations **must be identical** in this regard when it comes to anything that could be communicated between the client(s) and server. Otherwise, you could end up with messages being sent to override instances that don't know how to handle them! When using more than one network prefab, it is important to understand that each client determines what prefab they will be using and will not be synchronized across other clients. This feature is primarily to be used for things like platform specific Network Prefabs where things like collision models or graphics related assets might need to vary between platforms.