using System.Collections.Generic; using UnityEngine; namespace Normal.Realtime { /// /// Forked version of RealtimePool that does NOT pool avatar prefabs. /// Avatars are destroyed normally to allow OnDestroy() to execute properly. /// All other prefabs are pooled as normal. /// /// Drop this beside a Realtime component to pool all prefab views managed by that Realtime component. /// It maintains one pool per prefab. Prefab instances are set to inactive when they're despawned. /// [RequireComponent(typeof(Realtime))] public class ForkedRealtimePool : MonoBehaviour, IRealtimePrefabInstantiateDelegate { /// /// Manages the pool for a specific prefab. /// Created on demand when a prefab is first spawned. /// private class Pool { /// /// The prefab that this pool manages. /// private readonly GameObject _prefab; /// /// Instances that are ready to be recycled. /// private readonly Queue _inactiveInstances = new Queue(); public Pool(GameObject prefab) { _prefab = prefab; } /// /// Creates or recycles a new instance. /// public GameObject AcquireFromPool() { if (TryRecycle(out var prefabInstance)) { return prefabInstance; } else { return Instantiate(_prefab); } } /// /// Returns an instance to the pool. /// public void ReturnToPool(GameObject prefabInstance) { _inactiveInstances.Enqueue(prefabInstance); // Deactivate // Also triggers OnDisable on components prefabInstance.SetActive(false); } /// /// Creates an amount of inactive instances and places them into the pool. /// public void Preload(int count) { for (var i = 0; i < count; i++) { var prefabInstance = Instantiate(_prefab); ReturnToPool(prefabInstance); } } // InstantiateAsync was backported to 2022.3.20f1. // But Unity only provides a UNITY_X_Y_OR_NEWER define (not UNITY_X_Y_Z_OR_NEWER). // So we can't safely compile it for 2022.3 and we use Unity 6 as the cutoff instead. #if UNITY_6000_0_OR_NEWER /// /// Creates an amount of inactive instances and places them into the pool using . /// public AsyncInstantiateOperation PreloadAsync(int count) { var operation = InstantiateAsync(_prefab, count); // Handle case where the operation has finished synchronously if (operation.isDone) { OnPreloadAsyncComplete(operation); } else { operation.completed += OnPreloadAsyncComplete; } return operation; } /// /// Processes the results of an AsyncInstantiateOperation. /// private void OnPreloadAsyncComplete(AsyncOperation operation) { var instantiateOperation = (AsyncInstantiateOperation)operation; foreach (var result in instantiateOperation.Result) { var prefabInstance = (GameObject)result; ReturnToPool(prefabInstance); } } #endif /// /// Destroys all inactive instances in the pool. /// public void Clear() { while (_inactiveInstances.Count > 0) { var prefabInstance = _inactiveInstances.Dequeue(); // Destroy the GameObject UnityEngine.Object.Destroy(prefabInstance); } } /// /// Tries to recycle an inactive instance. /// private bool TryRecycle(out GameObject prefabInstance) { if (_inactiveInstances.Count == 0) { prefabInstance = null; return false; } else { prefabInstance = _inactiveInstances.Dequeue(); // Activate // Also triggers OnEnable on components prefabInstance.SetActive(true); return true; } } /// /// Creates a new instance. /// private GameObject Instantiate(GameObject prefab) { // Instantiate the prefab return UnityEngine.Object.Instantiate(prefab); } } /// /// Maps a prefab to its using the prefab's name. /// private readonly Dictionary _pools = new Dictionary(); /// /// Used to avoid allocation for GetComponents. /// private static readonly List _tempPoolCallbacks = new List(20); /// /// When preloading prefabs by name, used to resolve the name to a prefab we can instantiate. /// private IRealtimePrefabLoadDelegate _prefabLoader; private void Awake() { // Use the same one that is being used by Realtime if (!TryGetComponent(out _prefabLoader)) { _prefabLoader = new DefaultRealtimePrefabDelegate(); } } /// public GameObject InstantiateRealtimePrefab(GameObject prefab) { var pool = GetOrCreatePool(prefab); var prefabInstance = pool.AcquireFromPool(); InvokeWillReuseFromPool(prefabInstance); return prefabInstance; } /// public void DestroyRealtimePrefab(GameObject prefabInstance) { // Check if this is an avatar prefab - if so, destroy it instead of pooling if (prefabInstance.GetComponent() != null) { // Invoke callbacks before destroying InvokeWillReturnToPool(prefabInstance); // Destroy the avatar normally to allow OnDestroy() to execute UnityEngine.Object.Destroy(prefabInstance); return; } // For non-avatar prefabs, use normal pooling behavior var rootView = prefabInstance.GetComponent(); var prefabName = rootView.prefabName; // Use the opposite order of InstantiateRealtimePrefab: // first InvokeOnWillReturnToPool and then ReturnToPool (which triggers OnDisable) InvokeWillReturnToPool(prefabInstance); var pool = GetPool(prefabName); pool.ReturnToPool(prefabInstance); } /// /// Creates an amount of inactive instances of a prefab and places them into the pool. /// /// /// Uses the specific used by the that this pool belongs to, /// or a default implementation if it doesn't have one. /// public void PreloadPrefab(string prefabName, int count) { RealtimePrefabMetadata prefabMetadata = new RealtimePrefabMetadata() { prefabName = prefabName, }; var prefab = _prefabLoader.LoadRealtimePrefab(prefabMetadata); PreloadPrefab(prefab, count); } /// /// Creates an amount of inactive instances of a prefab and places them into the pool. /// public void PreloadPrefab(GameObject prefab, int count) { var pool = GetOrCreatePool(prefab); pool.Preload(count); } #if UNITY_6000_0_OR_NEWER /// /// /// Creates an amount of inactive instances and places them into the pool using . /// /// /// /// This method avoids the possibility of dropping frames while preloading instances: /// Call it for each prefab you'd like to load and wait for all the operations to complete before connecting to a room. /// /// /// Uses the specific used by the that this pool belongs to, /// or a default implementation if it doesn't have one. /// public AsyncInstantiateOperation PreloadPrefabAsync(string prefabName, int count) { RealtimePrefabMetadata prefabMetadata = new RealtimePrefabMetadata() { prefabName = prefabName, }; var prefab = _prefabLoader.LoadRealtimePrefab(prefabMetadata); return PreloadPrefabAsync(prefab, count); } /// /// /// Creates an amount of inactive instances and places them into the pool using . /// /// /// /// This method avoids the possibility of dropping frames while preloading instances: /// Call it for each prefab you'd like to load and wait for all the operations to complete before connecting to a room. /// public AsyncInstantiateOperation PreloadPrefabAsync(GameObject prefab, int count) { var pool = GetOrCreatePool(prefab); return pool.PreloadAsync(count); } #endif /// /// Destroys all inactive instances of a prefab. /// public void Clear(GameObject prefab) { var prefabName = prefab.name; var pool = GetPool(prefabName); // We don't necessarily have a pool for this prefab, ex if InstantiateRealtimePrefab was never called for it pool?.Clear(); } /// /// Destroys all inactive instances of all prefabs. /// public void Clear() { foreach (var pair in _pools) { var pool = pair.Value; pool.Clear(); } } /// /// Finds an existing pool for the prefab (by name). /// private Pool GetPool(string prefabName) { return _pools[prefabName]; } /// /// Creates or finds an existing pool for the prefab. /// private Pool GetOrCreatePool(GameObject prefab) { var prefabName = prefab.name; if (_pools.TryGetValue(prefabName, out var pool) == false) { pool = new Pool(prefab); _pools[prefabName] = pool; } return pool; } /// /// Invokes on all components in the prefab instance root /// deriving from . /// private void InvokeWillReuseFromPool(GameObject prefabInstance) { prefabInstance.GetComponents(_tempPoolCallbacks); foreach (var poolCallback in _tempPoolCallbacks) { poolCallback.PrefabWillReuseFromPool(); } } /// /// Invokes on all components in the prefab instance root /// deriving from . /// private void InvokeWillReturnToPool(GameObject prefabInstance) { prefabInstance.GetComponents(_tempPoolCallbacks); foreach (var poolCallback in _tempPoolCallbacks) { poolCallback.PrefabWillReturnToPool(); } } } }