diff --git a/Assets/AddressablesPoolingBlueprint.md b/Assets/AddressablesPoolingBlueprint.md deleted file mode 100644 index f71fc550e5..0000000000 --- a/Assets/AddressablesPoolingBlueprint.md +++ /dev/null @@ -1,63 +0,0 @@ -# Unity Addressables Object Pooling System Blueprint - -## 1. System Context & Requirements -- **Environment:** Unity 3D, C#. -- **Genre:** Mobile MMORPG (Android, iOS). -- **Core Technologies:** Unity Addressables, Custom Object Pooling. -- **Problem Statement:** The current system uses direct `Instantiate` and `Destroy` with Addressables, causing CPU spikes, GC Allocation overhead, and FPS drops when handling many objects (e.g., from server packets). Memory management is tricky; releasing Addressables assets while objects are still alive causes missing asset errors, but not releasing them causes Out Of Memory (OOM) crashes. -- **Solution:** A parameter-driven Object Pooling system that recycles GameObjects and manages Addressables memory using a dynamic Time-To-Live (TTL) Reference Counting mechanism. No external config files (ScriptableObjects) are used; behaviors are dictated directly via parameters when calling the `Spawn` method. - ---- - -## 2. Core Architecture -The system consists of three main components: -1. **`IPoolable` (Interface):** Attached to prefab scripts. Handles internal state resets when an object is reused or returned. -2. **`ObjectPool` (Class):** Manages instances of a specific Addressables Asset. Tracks active instances, idle instances, and handles the Memory Release TTL countdown. -3. **`PoolManager` (Singleton/Service):** The centralized API for the Game/Server to request (`Spawn`) and return (`Despawn`) objects. - ---- - -## 3. Processing Flows - -### A. Spawn Flow -1. Caller requests an object via `PoolManager.Spawn(addressableKey, position, rotation, memoryReleaseTTL, autoDespawnTime)`. -2. `PoolManager` checks if an `ObjectPool` exists for the given `addressableKey`. If not, creates one. -3. `PoolManager` updates the pool's internal TTL value with the newly provided `memoryReleaseTTL`. -4. `ObjectPool` attempts to retrieve an idle GameObject: - - **If idle list is empty:** Loads the asset via `Addressables.LoadAssetAsync`, Instantiates it, and increments the Active Count. - - **If idle list has items:** Pops the last item, sets it Active, and increments the Active Count. -5. If the memory release TTL timer is currently running (because Active Count was previously 0), **Cancel the timer**. -6. The GameObject's `IPoolable.OnSpawn()` is called. -7. If `autoDespawnTime > 0`, `PoolManager` starts a Coroutine to automatically Despawn the object after the specified seconds. -8. Returns the GameObject to the caller. - -### B. Despawn Flow -1. Caller requests to return an object via `PoolManager.Despawn(addressableKey, gameObject)`. -2. `PoolManager` locates the corresponding `ObjectPool`. -3. The GameObject's `IPoolable.OnDespawn()` is called to reset states (e.g., clear trails, reset health). -4. GameObject is deactivated (`SetActive(false)`). -5. `ObjectPool` adds the GameObject to the idle list and decrements the Active Count. -6. **Trigger TTL Check:** If `Active Count == 0`, `ObjectPool` starts the Memory Release Timer using its current `memoryReleaseTTL` value. - -### C. Memory Release Flow (Garbage Collection) -1. When Active Count reaches 0, a Coroutine (`ReleaseMemoryCountdown`) starts. -2. The Coroutine waits for `memoryReleaseTTL` seconds. -3. **Interruption:** If a new `Spawn` request for this key comes in during the wait, the Coroutine is stopped (Asset is kept in RAM). -4. **Completion:** If the Coroutine finishes and Active Count is still 0: - - Destroy all GameObjects in the idle list. - - Call `Addressables.ReleaseInstance()` or `Addressables.Release()` to free the RAM. - - Remove the `ObjectPool` from the `PoolManager`'s dictionary. - ---- - -## 4. C# Code Models (Definitions & Stubs) - -*Note for AI Code Generator: Use these class signatures to implement the full functional code.* - -### I. IPoolable.cs -```csharp -public interface IPoolable -{ - void OnSpawn(); - void OnDespawn(); -} \ No newline at end of file diff --git a/Assets/AddressablesPoolingBlueprint.md.meta b/Assets/AddressablesPoolingBlueprint.md.meta deleted file mode 100644 index 0e3544419c..0000000000 --- a/Assets/AddressablesPoolingBlueprint.md.meta +++ /dev/null @@ -1,7 +0,0 @@ -fileFormatVersion: 2 -guid: 6babe48248f2d2e4486f19a1908cc004 -TextScriptImporter: - externalObjects: {} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/PerfectWorld/Scripts/Managers/EC_ManMatter.cs b/Assets/PerfectWorld/Scripts/Managers/EC_ManMatter.cs index 6860f41640..aff1af0f7e 100644 --- a/Assets/PerfectWorld/Scripts/Managers/EC_ManMatter.cs +++ b/Assets/PerfectWorld/Scripts/Managers/EC_ManMatter.cs @@ -1,5 +1,6 @@ using BrewMonster; using BrewMonster.Network; +using BrewMonster.Scripts; using BrewMonster.Scripts.World; using CSNetwork; using CSNetwork.GPDataType; @@ -260,7 +261,8 @@ namespace PerfectWorld.Scripts.Managers CECMatter pMatter = GetMatter(mid); if (pMatter != null) { - UnityEngine.Object.Destroy(pMatter.gameObject); + //UnityEngine.Object.Destroy(pMatter.gameObject); + PoolManager.Instance.Despawn(pMatter.gameObject); m_MatterTab.Remove(mid); } //TODO: Might need to implement later @@ -345,4 +347,4 @@ namespace PerfectWorld.Scripts.Managers return null; } } -} \ No newline at end of file +} diff --git a/Assets/PerfectWorld/Scripts/Objet/CECMatter.cs b/Assets/PerfectWorld/Scripts/Objet/CECMatter.cs index b7b313e319..dbdb6bf855 100644 --- a/Assets/PerfectWorld/Scripts/Objet/CECMatter.cs +++ b/Assets/PerfectWorld/Scripts/Objet/CECMatter.cs @@ -161,10 +161,11 @@ namespace PerfectWorld.Scripts var fileMatterValue = fileMatterField.GetValue(matterData); string filePath = ByteToStringUtils.ByteArrayToCP936String((byte[])fileMatterValue); - var matterPrefab = await AddressableManager.Instance.LoadPrefabAsync(AFile.NormalizePath(filePath.ToLower(), true)); - if (matterPrefab != null) + //var matterPrefab = await AddressableManager.Instance.LoadPrefabAsync(AFile.NormalizePath(filePath.ToLower(), true)); + var matterObject = await PoolManager.Instance.SpawnAsync(AFile.NormalizePath(filePath.ToLower(), true), Vector3.zero, Quaternion.identity, 15f); + if (matterObject != null) { - var matterObject = Instantiate(matterPrefab); + //var matterObject = Instantiate(matterPrefab); matterObject.name = $"Matter {matterObject.name} {matterInfo.tid} {matterInfo.mid}"; matterObject.transform.position = new Vector3(Info.pos.x, Info.pos.y, Info.pos.z); matterObject.transform.localScale = new Vector3(1f, 1f, 1f); diff --git a/Assets/PerfectWorld/Scripts/PoolingManager/IPoolable.cs.meta b/Assets/PerfectWorld/Scripts/PoolingManager/IPoolable.cs.meta index c9dbfb1b97..0c0f5b2b61 100644 --- a/Assets/PerfectWorld/Scripts/PoolingManager/IPoolable.cs.meta +++ b/Assets/PerfectWorld/Scripts/PoolingManager/IPoolable.cs.meta @@ -1,2 +1,2 @@ fileFormatVersion: 2 -guid: 7c09b2b8b565f2946bb4e9f09e2966af \ No newline at end of file +guid: 7c09b2b8b565f2946bb4e9f09e2966af diff --git a/Assets/PerfectWorld/Scripts/PoolingManager/ObjectPool.cs b/Assets/PerfectWorld/Scripts/PoolingManager/ObjectPool.cs index 20ba26d011..2423b5c286 100644 --- a/Assets/PerfectWorld/Scripts/PoolingManager/ObjectPool.cs +++ b/Assets/PerfectWorld/Scripts/PoolingManager/ObjectPool.cs @@ -2,8 +2,6 @@ using System.Collections; using System.Collections.Generic; using System.Threading.Tasks; using UnityEngine; -using UnityEngine.AddressableAssets; -using UnityEngine.ResourceManagement.AsyncOperations; namespace BrewMonster.Scripts { @@ -11,22 +9,28 @@ namespace BrewMonster.Scripts { private readonly string _addressableKey; private readonly PoolManager _owner; + private readonly AddressableManager _addressableManager; private readonly Transform _poolRoot; private readonly Stack _idleInstances = new(); private readonly HashSet _activeInstances = new(); private readonly HashSet _knownInstances = new(); private readonly Dictionary _spawnVersions = new(); - private AsyncOperationHandle _prefabHandle; private GameObject _prefab; private Task _loadTask; private Coroutine _releaseCoroutine; private float _memoryReleaseTTL; - public ObjectPool(string addressableKey, PoolManager owner, Transform poolRoot, float memoryReleaseTTL) + public ObjectPool( + string addressableKey, + PoolManager owner, + AddressableManager addressableManager, + Transform poolRoot, + float memoryReleaseTTL) { _addressableKey = addressableKey; _owner = owner; + _addressableManager = addressableManager; _poolRoot = poolRoot; _memoryReleaseTTL = Mathf.Max(0f, memoryReleaseTTL); } @@ -111,7 +115,7 @@ namespace BrewMonster.Scripts { CancelReleaseCountdown(); DestroyAllInstances(); - ReleasePrefabHandle(); + ReleasePrefabAsset(); } private GameObject GetIdleInstance() @@ -152,17 +156,22 @@ namespace BrewMonster.Scripts private async Task LoadPrefabInternalAsync() { - _prefabHandle = Addressables.LoadAssetAsync(_addressableKey); - await _prefabHandle.Task; - - if (_prefabHandle.Status != AsyncOperationStatus.Succeeded || _prefabHandle.Result == null) + if (_addressableManager == null) { - Debug.LogError($"ObjectPool: Failed to load Addressable prefab '{_addressableKey}'."); - ReleasePrefabHandle(); + BMLogger.LogError($"ObjectPool: AddressableManager is not available for '{_addressableKey}'."); return null; } - _prefab = _prefabHandle.Result; + await _addressableManager.WaitUntilInitializedAsync(); + + GameObject prefab = await _addressableManager.LoadPrefabAsync(_addressableKey); + if (prefab == null) + { + BMLogger.LogError($"ObjectPool: Failed to load Addressable prefab '{_addressableKey}'."); + return null; + } + + _prefab = prefab; return _prefab; } @@ -187,7 +196,7 @@ namespace BrewMonster.Scripts { if (_memoryReleaseTTL > 0f) { - yield return new WaitForSeconds(_memoryReleaseTTL); + yield return new WaitForSecondsRealtime(_memoryReleaseTTL); } _releaseCoroutine = null; @@ -198,22 +207,24 @@ namespace BrewMonster.Scripts _owner.RemovePool(this); DestroyAllInstances(); - ReleasePrefabHandle(); + ReleasePrefabAsset(); } private void DestroyAllInstances() { foreach (GameObject instance in _knownInstances) { - if (instance != null) + if (instance == null) { - if (_activeInstances.Contains(instance)) - { - NotifyPoolablesDespawned(instance); - } - - Object.Destroy(instance); + continue; } + + if (_activeInstances.Contains(instance)) + { + NotifyPoolablesDespawned(instance); + } + + Object.Destroy(instance); } _idleInstances.Clear(); @@ -227,14 +238,13 @@ namespace BrewMonster.Scripts } } - private void ReleasePrefabHandle() + private void ReleasePrefabAsset() { - if (_prefabHandle.IsValid()) + if (_prefab != null && _addressableManager != null) { - Addressables.Release(_prefabHandle); + _addressableManager.ReleaseAsset(_addressableKey); } - _prefabHandle = default; _prefab = null; _loadTask = null; } diff --git a/Assets/PerfectWorld/Scripts/PoolingManager/ObjectPool.cs.meta b/Assets/PerfectWorld/Scripts/PoolingManager/ObjectPool.cs.meta index 3054953278..691cec0bfa 100644 --- a/Assets/PerfectWorld/Scripts/PoolingManager/ObjectPool.cs.meta +++ b/Assets/PerfectWorld/Scripts/PoolingManager/ObjectPool.cs.meta @@ -1,2 +1,2 @@ fileFormatVersion: 2 -guid: 8fc34059fd203384d9c5aa022301e68f \ No newline at end of file +guid: 8fc34059fd203384d9c5aa022301e68f diff --git a/Assets/PerfectWorld/Scripts/PoolingManager/PoolManager.cs b/Assets/PerfectWorld/Scripts/PoolingManager/PoolManager.cs index 5b2646be62..4ff5714461 100644 --- a/Assets/PerfectWorld/Scripts/PoolingManager/PoolManager.cs +++ b/Assets/PerfectWorld/Scripts/PoolingManager/PoolManager.cs @@ -10,11 +10,17 @@ namespace BrewMonster.Scripts { private readonly Dictionary _pools = new(); private readonly Dictionary _instanceToPool = new(); + + private AddressableManager _addressableManager; private Transform _poolContainer; protected override void Initialize() { base.Initialize(); + + _addressableManager = AddressableManager.Instance; + _addressableManager.OnDispose += ReleaseAllPools; + _poolContainer = new GameObject("Addressables Object Pools").transform; _poolContainer.SetParent(transform, false); } @@ -32,7 +38,7 @@ namespace BrewMonster.Scripts { if (string.IsNullOrEmpty(addressableKey)) { - Debug.LogError("PoolManager: Cannot spawn with a null or empty Addressables key."); + BMLogger.LogError("PoolManager: Cannot spawn with a null or empty Addressables key."); return null; } @@ -168,7 +174,7 @@ namespace BrewMonster.Scripts if (spawnTask.Exception != null) { - Debug.LogException(spawnTask.Exception); + BMLogger.LogError($"PoolManager: Spawn failed for '{addressableKey}': {spawnTask.Exception}"); onComplete?.Invoke(null); yield break; } @@ -196,7 +202,7 @@ namespace BrewMonster.Scripts Transform poolRoot = new GameObject(GetPoolRootName(addressableKey)).transform; poolRoot.SetParent(_poolContainer, false); - pool = new ObjectPool(addressableKey, this, poolRoot, memoryReleaseTTL); + pool = new ObjectPool(addressableKey, this, _addressableManager, poolRoot, memoryReleaseTTL); _pools[addressableKey] = pool; return pool; } @@ -226,6 +232,11 @@ namespace BrewMonster.Scripts protected override void OnDestroy() { + if (_addressableManager != null) + { + _addressableManager.OnDispose -= ReleaseAllPools; + } + ReleaseAllPools(); base.OnDestroy(); } diff --git a/Assets/PerfectWorld/Scripts/PoolingManager/PoolManager.cs.meta b/Assets/PerfectWorld/Scripts/PoolingManager/PoolManager.cs.meta index f4c76c06ac..b215665d99 100644 --- a/Assets/PerfectWorld/Scripts/PoolingManager/PoolManager.cs.meta +++ b/Assets/PerfectWorld/Scripts/PoolingManager/PoolManager.cs.meta @@ -1,2 +1,2 @@ fileFormatVersion: 2 -guid: b8e790a183861dd40bd580421c793038 \ No newline at end of file +guid: b8e790a183861dd40bd580421c793038 diff --git a/Assets/PerfectWorld/Scripts/PoolingManager/poolingManager.md.meta b/Assets/PerfectWorld/Scripts/PoolingManager/poolingManager.md.meta deleted file mode 100644 index 1a41a199c1..0000000000 --- a/Assets/PerfectWorld/Scripts/PoolingManager/poolingManager.md.meta +++ /dev/null @@ -1,7 +0,0 @@ -fileFormatVersion: 2 -guid: 191a414af7b325d459e32ac5254c011d -TextScriptImporter: - externalObjects: {} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Documentation/addressable-manager.md b/Documentation/addressable-manager.md new file mode 100644 index 0000000000..3e331550f4 --- /dev/null +++ b/Documentation/addressable-manager.md @@ -0,0 +1,201 @@ +# AddressableManager Public API + +`AddressableManager` is a Unity `MonoSingleton` wrapper around Unity Addressables. It initializes the Addressables system, loads selected asset types, caches loaded handles, and exposes methods for delayed or immediate release. + +This document lists only the public functions from `Assets/PerfectWorld/Scripts/Addressable/AddressableManager.cs`. + +## `bool IsInitialized()` + +Returns whether the Addressables initialization callback has completed successfully. + +Use this when another system needs to quickly check if `AddressableManager` is ready before requesting assets. It returns `true` only after `Addressables.InitializeAsync()` finishes with `AsyncOperationStatus.Succeeded`. + +If initialization fails, this method remains `false`. + +## `UniTask WaitUntilInitializedAsync()` + +Waits until `AddressableManager` has finished initializing Unity Addressables. + +If initialization has already completed, this returns `UniTask.CompletedTask` immediately. Otherwise, it waits on the internal initialization completion source that is resolved by the Addressables initialization callback. + +Call this before loading assets from systems that may start before Addressables is ready. + +Example usage: + +```csharp +await AddressableManager.Instance.WaitUntilInitializedAsync(); +``` + +## `Task LoadTextAssetAsync(string assetPath)` + +Loads a `TextAsset` asynchronously through Unity Addressables. + +`assetPath` must match a valid Addressables key, normally the asset address. The method first increases the internal reference count for this path and removes the path from the delayed-release list so the asset will not be released while it is being used again. + +If the text asset was already loaded and the cached handle is still valid, the method returns the cached `TextAsset` immediately. If no valid cached handle exists, it calls: + +```csharp +Addressables.LoadAssetAsync(assetPath) +``` + +After loading completes, the handle is stored in the text asset cache and the loaded `TextAsset` is returned. + +If loading throws an exception, the method logs an error and returns `null`. + +Important behavior: + +- The loaded asset is cached by `assetPath`. +- Repeated calls for the same valid path reuse the cached handle. +- The method may return `null` when the asset cannot be loaded. +- Release is not automatic after loading. Call a release method when the asset is no longer needed. + +## `Task LoadPrefabAsync(string assetPath)` + +Loads a prefab asynchronously through Unity Addressables and returns it as a `GameObject`. + +`assetPath` must match a valid Addressables key for a `GameObject` prefab. Before loading, the method removes the path from the delayed-release list. It then calls `KeyExists(assetPath, typeof(GameObject))`; if the key is considered missing, it logs a warning and returns `null`. + +If the prefab is already cached and the cached handle is valid, the method returns the cached prefab immediately. Otherwise, it calls: + +```csharp +Addressables.LoadAssetAsync(assetPath) +``` + +After loading completes successfully, the handle is stored in the prefab cache and the prefab `GameObject` is returned. + +If the Addressables operation reports an exception, or if an exception is thrown while loading, the method logs the failure and returns `null`. In the Unity Editor, failed paths are added to an invalid-path cache so later `KeyExists` checks can reject them quickly. + +Important behavior: + +- This loads the prefab asset itself, not an instantiated scene object. +- Callers that need an instance should instantiate the returned prefab separately. +- The loaded prefab handle is cached by `assetPath`. +- The method may return `null` when the key does not exist or loading fails. + +## `Task LoadAudioClipAsync(string assetPath)` + +Loads an `AudioClip` asynchronously through Unity Addressables. + +`assetPath` must match a valid Addressables key for an audio asset. The method removes the path from the delayed-release list before checking the cache. + +If the audio clip is already cached and the cached handle is valid, the method returns the cached `AudioClip` immediately. Otherwise, it calls: + +```csharp +Addressables.LoadAssetAsync(assetPath.Trim()) +``` + +After loading completes successfully, the handle is stored in the audio cache and the loaded `AudioClip` is returned. + +If the Addressables operation reports an exception, or if an exception is thrown while loading, the method logs the failure and returns `null`. + +Important behavior: + +- The Addressables load uses `assetPath.Trim()`. +- The cache key is the original `assetPath` string. +- Repeated calls for the same cached path can return immediately. +- The method may return `null` when loading fails. + +## `bool TryGetCachedAudioClip(string assetPath, out AudioClip clip)` + +Attempts to retrieve an already loaded `AudioClip` from the internal audio cache without starting a new Addressables load. + +If `assetPath` is null or empty, the method returns `false` and sets `clip` to `null`. + +If the audio cache contains a valid handle for `assetPath` and the handle has a non-null result, the method assigns that cached `AudioClip` to `clip` and returns `true`. + +If no valid cached audio clip exists, the method returns `false` and leaves `clip` as `null`. + +Use this when a caller wants to play audio immediately if it is already loaded, while avoiding an async load path. + +## `void ReleaseAsset(string assetPath)` + +Marks an asset path for delayed release. + +This method does not immediately call `Addressables.Release`. Instead, it records a timestamp in the delayed-release dictionary. During `Update`, once the configured timeout has elapsed, the manager force releases the asset by path. + +If the same path is released multiple times before it is force released, the earliest release timestamp is kept. + +Important behavior: + +- This is a delayed release request. +- The asset remains cached until the release timeout expires. +- If the asset is loaded again before timeout, load methods remove it from the delayed-release list. +- The path must match the same key used to cache the asset. + +## `void ForceReleaseAsset(string assetPath)` + +Immediately releases a cached asset by path. + +The method checks the prefab cache first, then the text asset cache, then the audio cache. If the path exists in one of those caches and the stored handle is valid, it calls: + +```csharp +Addressables.Release(handle) +``` + +After releasing, the method removes the path from the matching cache. For audio assets, it also logs that the audio asset was force released. + +Important behavior: + +- This releases immediately, unlike `ReleaseAsset(string assetPath)`. +- Only one matching cache entry is released because the method uses an `if / else if` chain. +- If the path is not found in any cache, the method does nothing. + +## `void ReleaseAsset(AsyncOperationHandle handle)` + +Releases a specific `GameObject` Addressables operation handle directly. + +If the provided handle is valid, the method calls: + +```csharp +Addressables.Release(handle) +``` + +This overload does not remove entries from `AddressableManager`'s internal prefab cache because it does not know which `assetPath` the handle belongs to. Prefer releasing by `assetPath` when the asset was loaded through `AddressableManager` and should also be removed from its cache. + +## `void ReleaseAllAssets()` + +Immediately releases all cached prefab and audio assets. + +The method iterates through the prefab cache and releases each valid prefab handle. It then clears the prefab cache. After that, it iterates through the audio cache, releases each valid audio handle, and clears the audio cache. + +Important behavior: + +- This releases prefab and audio caches. +- It does not currently release cached text assets from `_loadedTextAssets`. +- It logs `"AddressableManager: Released all assets"` after completing. +- `OnDestroy` calls this method automatically. + +## `bool IsAssetLoaded(string assetPath)` + +Checks whether a prefab asset path is currently loaded in the prefab cache. + +The method returns `true` only when `_loadedPrefabAssets` contains `assetPath` and the stored handle is valid. + +Important behavior: + +- This checks only loaded prefab assets. +- It does not check cached text assets. +- It does not check cached audio assets. + +## `static bool KeyExists(object key, System.Type type = null)` + +Checks whether an Addressables key exists. + +In non-editor builds, the method iterates through all loaded Addressables resource locators and calls: + +```csharp +locator.Locate(key, type, out var locations) +``` + +It returns `true` when a locator finds at least one matching resource location. If no locator finds a match, it returns `false`. + +The optional `type` parameter can restrict the lookup to a specific asset type, such as `typeof(GameObject)`. + +In the Unity Editor, this method currently returns `true` unless the key string exists in the manager's `_invalidAssetPaths` set. That means editor behavior is based on paths that previously failed to load, not a full catalog lookup. + +Important behavior: + +- Runtime builds perform a locator-based catalog lookup. +- Editor builds use the invalid-path cache. +- `LoadPrefabAsync` uses this method before attempting to load a prefab. +- Passing a type helps avoid matching an address that exists for a different asset kind. diff --git a/Assets/PerfectWorld/Scripts/PoolingManager/poolingManager.md b/Documentation/poolingManager.md similarity index 71% rename from Assets/PerfectWorld/Scripts/PoolingManager/poolingManager.md rename to Documentation/poolingManager.md index c3297c23ae..a586763d7e 100644 --- a/Assets/PerfectWorld/Scripts/PoolingManager/poolingManager.md +++ b/Documentation/poolingManager.md @@ -4,11 +4,13 @@ The pooling system recycles Addressables-backed `GameObject` instances instead of repeatedly calling `Instantiate` and `Destroy`. Each Addressables key owns one runtime pool that tracks active objects, idle objects, and a memory-release timer. +`PoolManager` loads prefab assets through `AddressableManager`, so it uses the same Addressables initialization path and loaded-prefab cache as the rest of the project. Runtime instances are ordinary instantiated `GameObject`s owned by the pool. + ## Runtime Files - `IPoolable.cs`: prefab-side lifecycle interface. -- `ObjectPool.cs`: owns instances and the loaded Addressables prefab handle for one key. -- `PoolManager.cs`: singleton service used by gameplay, server packet handlers, and UI code. +- `ObjectPool.cs`: owns instances and the loaded prefab reference for one Addressables key. +- `PoolManager.cs`: singleton service used by gameplay, server packet handlers, UI code, and effects systems. ## Spawn Flow @@ -17,9 +19,9 @@ The pooling system recycles Addressables-backed `GameObject` instances instead o 3. The pool updates its `memoryReleaseTTL` from the spawn parameter. 4. If a memory release countdown is running, the pool cancels it. 5. The pool reuses an idle object when available. -6. If no idle object exists, the pool loads the prefab with `Addressables.LoadAssetAsync()` and instantiates it. +6. If no idle object exists, the pool waits for `AddressableManager` initialization, loads the prefab with `AddressableManager.LoadPrefabAsync()`, then instantiates it. 7. The instance is parented, positioned, rotated, activated, and all `IPoolable.OnSpawn()` hooks are called. -8. If `autoDespawnTime > 0`, `PoolManager` starts a version-checked auto despawn coroutine. +8. If `autoDespawnTime > 0`, `PoolManager` starts a version-checked auto-despawn coroutine. 9. The spawned `GameObject` is returned to the caller. ## Despawn Flow @@ -33,17 +35,21 @@ The pooling system recycles Addressables-backed `GameObject` instances instead o ## Memory Release Flow 1. The countdown waits for the latest `memoryReleaseTTL` value supplied by spawn calls for that key. -2. A new spawn for the same key cancels the countdown and keeps the prefab handle in memory. +2. A new spawn for the same key cancels the countdown and keeps the prefab plus idle instances available. 3. If the countdown completes while active count is still zero, the pool: - unregisters itself from `PoolManager`; - destroys all pooled instances; - - releases the Addressables prefab handle with `Addressables.Release()`; + - calls `AddressableManager.ForceReleaseAsset(addressableKey)` to release the cached prefab asset; - destroys the pool root object. ## Auto Despawn Safety Auto despawn stores the instance spawn version when the coroutine starts. If the object is manually despawned and reused before the timer completes, the version changes and the old coroutine will not despawn the new lifecycle. +## Shutdown Flow + +`PoolManager` subscribes to `AddressableManager.OnDispose`. When the Addressables manager is disposed, all pools release their instances and prefab assets before `AddressableManager.ReleaseAllAssets()` runs. `PoolManager.OnDestroy()` also releases all pools as a fallback. + ## Example ```csharp