Merge branch 'develop' into feature/loganimation-and-stun-effect

This commit is contained in:
vuong dinh hoang
2026-05-04 17:12:07 +07:00
13 changed files with 1093 additions and 5 deletions
@@ -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;
}
}
}
}
@@ -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);
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: a86dd2e474fb3c34cb8214c766f0b941
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,11 @@
namespace BrewMonster.Scripts
{
/// <summary>
/// Implement this on pooled prefab components that need to reset state between uses.
/// </summary>
public interface IPoolable
{
void OnSpawn();
void OnDespawn();
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 7c09b2b8b565f2946bb4e9f09e2966af
@@ -0,0 +1,270 @@
using System.Collections;
using System.Collections.Generic;
using System.Threading.Tasks;
using UnityEngine;
namespace BrewMonster.Scripts
{
internal sealed class ObjectPool
{
private readonly string _addressableKey;
private readonly PoolManager _owner;
private readonly AddressableManager _addressableManager;
private readonly Transform _poolRoot;
private readonly Stack<GameObject> _idleInstances = new();
private readonly HashSet<GameObject> _activeInstances = new();
private readonly HashSet<GameObject> _knownInstances = new();
private readonly Dictionary<GameObject, int> _spawnVersions = new();
private GameObject _prefab;
private Task<GameObject> _loadTask;
private Coroutine _releaseCoroutine;
private 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);
}
public string AddressableKey => _addressableKey;
public int ActiveCount => _activeInstances.Count;
public int IdleCount => _idleInstances.Count;
public void UpdateMemoryReleaseTTL(float memoryReleaseTTL)
{
_memoryReleaseTTL = Mathf.Max(0f, memoryReleaseTTL);
}
public async Task<GameObject> SpawnAsync(Vector3 position, Quaternion rotation, Transform parent)
{
CancelReleaseCountdown();
GameObject instance = GetIdleInstance();
if (instance == null)
{
GameObject prefab = await LoadPrefabAsync();
if (prefab == null)
{
return null;
}
instance = Object.Instantiate(prefab);
_knownInstances.Add(instance);
_spawnVersions[instance] = 0;
}
_activeInstances.Add(instance);
_spawnVersions[instance]++;
instance.transform.SetParent(parent, true);
instance.transform.SetPositionAndRotation(position, rotation);
instance.SetActive(true);
NotifyPoolablesSpawned(instance);
return instance;
}
public bool Despawn(GameObject instance)
{
if (instance == null || !_activeInstances.Remove(instance))
{
return false;
}
NotifyPoolablesDespawned(instance);
_spawnVersions[instance]++;
instance.SetActive(false);
instance.transform.SetParent(_poolRoot, false);
_idleInstances.Push(instance);
if (_activeInstances.Count == 0)
{
StartReleaseCountdown();
}
return true;
}
public bool IsActiveInstance(GameObject instance, int spawnVersion)
{
return instance != null
&& _activeInstances.Contains(instance)
&& _spawnVersions.TryGetValue(instance, out int currentVersion)
&& currentVersion == spawnVersion;
}
public int GetSpawnVersion(GameObject instance)
{
return instance != null && _spawnVersions.TryGetValue(instance, out int version) ? version : -1;
}
public IEnumerable<GameObject> GetKnownInstances()
{
return _knownInstances;
}
public void ReleaseNow()
{
CancelReleaseCountdown();
DestroyAllInstances();
ReleasePrefabAsset();
}
private GameObject GetIdleInstance()
{
while (_idleInstances.Count > 0)
{
GameObject instance = _idleInstances.Pop();
if (instance != null)
{
return instance;
}
}
return null;
}
private async Task<GameObject> LoadPrefabAsync()
{
if (_prefab != null)
{
return _prefab;
}
if (_loadTask != null)
{
return await _loadTask;
}
_loadTask = LoadPrefabInternalAsync();
GameObject prefab = await _loadTask;
if (prefab == null)
{
_loadTask = null;
}
return prefab;
}
private async Task<GameObject> LoadPrefabInternalAsync()
{
if (_addressableManager == null)
{
BMLogger.LogError($"ObjectPool: AddressableManager is not available for '{_addressableKey}'.");
return null;
}
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;
}
private void StartReleaseCountdown()
{
CancelReleaseCountdown();
_releaseCoroutine = _owner.StartCoroutine(ReleaseMemoryCountdown());
}
private void CancelReleaseCountdown()
{
if (_releaseCoroutine == null)
{
return;
}
_owner.StopCoroutine(_releaseCoroutine);
_releaseCoroutine = null;
}
private IEnumerator ReleaseMemoryCountdown()
{
if (_memoryReleaseTTL > 0f)
{
yield return new WaitForSecondsRealtime(_memoryReleaseTTL);
}
_releaseCoroutine = null;
if (_activeInstances.Count > 0)
{
yield break;
}
_owner.RemovePool(this);
DestroyAllInstances();
ReleasePrefabAsset();
}
private void DestroyAllInstances()
{
foreach (GameObject instance in _knownInstances)
{
if (instance == null)
{
continue;
}
if (_activeInstances.Contains(instance))
{
NotifyPoolablesDespawned(instance);
}
Object.Destroy(instance);
}
_idleInstances.Clear();
_activeInstances.Clear();
_knownInstances.Clear();
_spawnVersions.Clear();
if (_poolRoot != null)
{
Object.Destroy(_poolRoot.gameObject);
}
}
private void ReleasePrefabAsset()
{
if (_prefab != null && _addressableManager != null)
{
_addressableManager.ReleaseAsset(_addressableKey);
}
_prefab = null;
_loadTask = null;
}
private static void NotifyPoolablesSpawned(GameObject instance)
{
IPoolable[] poolables = instance.GetComponentsInChildren<IPoolable>(true);
for (int i = 0; i < poolables.Length; i++)
{
poolables[i].OnSpawn();
}
}
private static void NotifyPoolablesDespawned(GameObject instance)
{
IPoolable[] poolables = instance.GetComponentsInChildren<IPoolable>(true);
for (int i = 0; i < poolables.Length; i++)
{
poolables[i].OnDespawn();
}
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 8fc34059fd203384d9c5aa022301e68f
@@ -0,0 +1,244 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Threading.Tasks;
using UnityEngine;
namespace BrewMonster.Scripts
{
public sealed class PoolManager : MonoSingleton<PoolManager>
{
private readonly Dictionary<string, ObjectPool> _pools = new();
private readonly Dictionary<GameObject, ObjectPool> _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);
}
/// <summary>
/// Spawns an Addressables prefab from its pool. The returned task completes after the prefab is loaded if needed.
/// </summary>
public async Task<GameObject> SpawnAsync(
string addressableKey,
Vector3 position,
Quaternion rotation,
float memoryReleaseTTL,
float autoDespawnTime = 0f,
Transform parent = null)
{
if (string.IsNullOrEmpty(addressableKey))
{
BMLogger.LogError("PoolManager: Cannot spawn with a null or empty Addressables key.");
return null;
}
ObjectPool pool = GetOrCreatePool(addressableKey, memoryReleaseTTL);
pool.UpdateMemoryReleaseTTL(memoryReleaseTTL);
GameObject instance = await pool.SpawnAsync(position, rotation, parent);
if (instance == null)
{
return null;
}
_instanceToPool[instance] = pool;
if (autoDespawnTime > 0f)
{
StartCoroutine(AutoDespawnAfter(pool, instance, pool.GetSpawnVersion(instance), autoDespawnTime));
}
return instance;
}
/// <summary>
/// Coroutine-friendly spawn API for callers that do not use async/await.
/// </summary>
public void Spawn(
string addressableKey,
Vector3 position,
Quaternion rotation,
float memoryReleaseTTL,
float autoDespawnTime,
Action<GameObject> onComplete,
Transform parent = null)
{
StartCoroutine(SpawnRoutine(addressableKey, position, rotation, memoryReleaseTTL, autoDespawnTime, onComplete, parent));
}
public bool Despawn(string addressableKey, GameObject instance)
{
if (string.IsNullOrEmpty(addressableKey) || instance == null)
{
return false;
}
if (!_pools.TryGetValue(addressableKey, out ObjectPool pool))
{
return false;
}
bool despawned = pool.Despawn(instance);
if (despawned)
{
_instanceToPool[instance] = pool;
}
return despawned;
}
public bool Despawn(GameObject instance)
{
if (instance == null || !_instanceToPool.TryGetValue(instance, out ObjectPool pool))
{
return false;
}
return pool.Despawn(instance);
}
public bool TryGetPoolCounts(string addressableKey, out int activeCount, out int idleCount)
{
activeCount = 0;
idleCount = 0;
if (!_pools.TryGetValue(addressableKey, out ObjectPool pool))
{
return false;
}
activeCount = pool.ActiveCount;
idleCount = pool.IdleCount;
return true;
}
public void ReleasePool(string addressableKey)
{
if (!_pools.TryGetValue(addressableKey, out ObjectPool pool))
{
return;
}
UnregisterPoolInstances(pool);
pool.ReleaseNow();
_pools.Remove(addressableKey);
}
public void ReleaseAllPools()
{
List<ObjectPool> pools = new(_pools.Values);
for (int i = 0; i < pools.Count; i++)
{
pools[i].ReleaseNow();
}
_pools.Clear();
_instanceToPool.Clear();
}
internal void RemovePool(ObjectPool pool)
{
if (pool == null)
{
return;
}
UnregisterPoolInstances(pool);
_pools.Remove(pool.AddressableKey);
}
private IEnumerator SpawnRoutine(
string addressableKey,
Vector3 position,
Quaternion rotation,
float memoryReleaseTTL,
float autoDespawnTime,
Action<GameObject> onComplete,
Transform parent)
{
Task<GameObject> spawnTask = SpawnAsync(addressableKey, position, rotation, memoryReleaseTTL, autoDespawnTime, parent);
while (!spawnTask.IsCompleted)
{
yield return null;
}
if (spawnTask.Exception != null)
{
BMLogger.LogError($"PoolManager: Spawn failed for '{addressableKey}': {spawnTask.Exception}");
onComplete?.Invoke(null);
yield break;
}
onComplete?.Invoke(spawnTask.Result);
}
private IEnumerator AutoDespawnAfter(ObjectPool pool, GameObject instance, int spawnVersion, float autoDespawnTime)
{
yield return new WaitForSeconds(autoDespawnTime);
if (pool != null && pool.IsActiveInstance(instance, spawnVersion))
{
Despawn(instance);
}
}
private ObjectPool GetOrCreatePool(string addressableKey, float memoryReleaseTTL)
{
if (_pools.TryGetValue(addressableKey, out ObjectPool pool))
{
return pool;
}
Transform poolRoot = new GameObject(GetPoolRootName(addressableKey)).transform;
poolRoot.SetParent(_poolContainer, false);
pool = new ObjectPool(addressableKey, this, _addressableManager, poolRoot, memoryReleaseTTL);
_pools[addressableKey] = pool;
return pool;
}
private void UnregisterPoolInstances(ObjectPool pool)
{
foreach (GameObject instance in pool.GetKnownInstances())
{
if (instance != null)
{
_instanceToPool.Remove(instance);
}
}
}
private static string GetPoolRootName(string addressableKey)
{
string name = addressableKey.Replace('\\', '/');
int lastSlash = name.LastIndexOf('/');
if (lastSlash >= 0 && lastSlash < name.Length - 1)
{
name = name.Substring(lastSlash + 1);
}
return $"Pool - {name}";
}
protected override void OnDestroy()
{
if (_addressableManager != null)
{
_addressableManager.OnDispose -= ReleaseAllPools;
}
ReleaseAllPools();
base.OnDestroy();
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: b8e790a183861dd40bd580421c793038
@@ -1,4 +1,5 @@
using BrewMonster.Network;
using CSNetwork;
using System.Collections.Generic;
using TMPro;
using UnityEngine;
@@ -20,6 +21,7 @@ namespace BrewMonster.UI
private bool m_bUpdateRenderTarget = false;
[SerializeField] TextMeshProUGUI skillNameText;
[SerializeField] Image imageProgress;
[SerializeField] Button btnCancel;
public virtual void Show(bool value)
{
@@ -313,6 +315,11 @@ namespace BrewMonster.UI
public virtual void Awake()
{
m_szName = "Dialog_";
if(btnCancel != null)
{
btnCancel.onClick.RemoveAllListeners();
btnCancel.onClick.AddListener(OnClickBtnCancel);
}
}
public virtual void Start()
@@ -365,5 +372,10 @@ namespace BrewMonster.UI
{
CECUIManager.Instance.HideCurrentUIInStack();
}
public void OnClickBtnCancel()
{
EC_ManMessage.PostMessage(EC_MsgDef.MSG_HST_PRESSCANCEL, MANAGER_INDEX.MAN_PLAYER, 0);
}
}
}
+259
View File
@@ -32,6 +32,7 @@ RectTransform:
- {fileID: 1925327431716214704}
- {fileID: 7830969636268911684}
- {fileID: 529978810702632783}
- {fileID: 1961303628433326950}
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 0}
@@ -53,6 +54,7 @@ MonoBehaviour:
m_EditorClassIdentifier:
skillNameText: {fileID: 4156282290771635373}
imageProgress: {fileID: 7818904766065936941}
btnCancel: {fileID: 4979337629572737255}
--- !u!1 &1961198748621413970
GameObject:
m_ObjectHideFlags: 0
@@ -189,6 +191,142 @@ MonoBehaviour:
m_hasFontAssetChanged: 0
m_baseMaterial: {fileID: 0}
m_maskOffset: {x: 0, y: 0, z: 0, w: 0}
--- !u!1 &3105178767150005615
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 1173275105690546560}
- component: {fileID: 2270321894074679265}
- component: {fileID: 3782302284108987037}
m_Layer: 0
m_Name: Text (TMP)
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!224 &1173275105690546560
RectTransform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 3105178767150005615}
m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 1961303628433326950}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 1, y: 1}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 0, y: 0}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!222 &2270321894074679265
CanvasRenderer:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 3105178767150005615}
m_CullTransparentMesh: 1
--- !u!114 &3782302284108987037
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 3105178767150005615}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: f4688fdb7df04437aeb418b961361dc5, type: 3}
m_Name:
m_EditorClassIdentifier:
m_Material: {fileID: 0}
m_Color: {r: 1, g: 1, b: 1, a: 1}
m_RaycastTarget: 1
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
m_Maskable: 1
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
m_text: Cancel
m_isRightToLeft: 0
m_fontAsset: {fileID: 11400000, guid: 369c2e14814cc9a4b8e3ad4e37769134, type: 2}
m_sharedMaterial: {fileID: 9092487103257209053, guid: 369c2e14814cc9a4b8e3ad4e37769134, type: 2}
m_fontSharedMaterials: []
m_fontMaterial: {fileID: 0}
m_fontMaterials: []
m_fontColor32:
serializedVersion: 2
rgba: 4281479730
m_fontColor: {r: 0.19607843, g: 0.19607843, b: 0.19607843, a: 1}
m_enableVertexGradient: 0
m_colorMode: 3
m_fontColorGradient:
topLeft: {r: 1, g: 1, b: 1, a: 1}
topRight: {r: 1, g: 1, b: 1, a: 1}
bottomLeft: {r: 1, g: 1, b: 1, a: 1}
bottomRight: {r: 1, g: 1, b: 1, a: 1}
m_fontColorGradientPreset: {fileID: 0}
m_spriteAsset: {fileID: 0}
m_tintAllSprites: 0
m_StyleSheet: {fileID: 0}
m_TextStyleHashCode: -1183493901
m_overrideHtmlColors: 0
m_faceColor:
serializedVersion: 2
rgba: 4294967295
m_fontSize: 24
m_fontSizeBase: 24
m_fontWeight: 400
m_enableAutoSizing: 0
m_fontSizeMin: 18
m_fontSizeMax: 72
m_fontStyle: 0
m_HorizontalAlignment: 2
m_VerticalAlignment: 512
m_textAlignment: 65535
m_characterSpacing: 0
m_wordSpacing: 0
m_lineSpacing: 0
m_lineSpacingMax: 0
m_paragraphSpacing: 0
m_charWidthMaxAdj: 0
m_TextWrappingMode: 1
m_wordWrappingRatios: 0.4
m_overflowMode: 0
m_linkedTextComponent: {fileID: 0}
parentLinkedComponent: {fileID: 0}
m_enableKerning: 0
m_ActiveFontFeatures: 6e72656b
m_enableExtraPadding: 0
checkPaddingRequired: 0
m_isRichText: 1
m_EmojiFallbackSupport: 1
m_parseCtrlCharacters: 1
m_isOrthographic: 1
m_isCullingEnabled: 0
m_horizontalMapping: 0
m_verticalMapping: 0
m_uvLineOffset: 0
m_geometrySortingOrder: 0
m_IsTextObjectScaleStatic: 0
m_VertexBufferAutoSizeReduction: 0
m_useMaxVisibleDescender: 1
m_pageToDisplay: 1
m_margin: {x: 0, y: 0, z: 0, w: 0}
m_isUsingLegacyAnimationComponent: 0
m_isVolumetricText: 0
m_hasFontAssetChanged: 0
m_baseMaterial: {fileID: 0}
m_maskOffset: {x: 0, y: 0, z: 0, w: 0}
--- !u!1 &4998799320591488598
GameObject:
m_ObjectHideFlags: 0
@@ -339,3 +477,124 @@ MonoBehaviour:
m_FillOrigin: 0
m_UseSpriteMesh: 0
m_PixelsPerUnitMultiplier: 1
--- !u!1 &8568909860233813620
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 1961303628433326950}
- component: {fileID: 1490671430424278683}
- component: {fileID: 4272345749132423406}
- component: {fileID: 4979337629572737255}
m_Layer: 0
m_Name: BtnCancel
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!224 &1961303628433326950
RectTransform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 8568909860233813620}
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 1173275105690546560}
m_Father: {fileID: 3062421795583818898}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0.5, y: 0.5}
m_AnchorMax: {x: 0.5, y: 0.5}
m_AnchoredPosition: {x: -329, y: -243}
m_SizeDelta: {x: 160, y: 30}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!222 &1490671430424278683
CanvasRenderer:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 8568909860233813620}
m_CullTransparentMesh: 1
--- !u!114 &4272345749132423406
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 8568909860233813620}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3}
m_Name:
m_EditorClassIdentifier:
m_Material: {fileID: 0}
m_Color: {r: 1, g: 1, b: 1, a: 1}
m_RaycastTarget: 1
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
m_Maskable: 1
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
m_Sprite: {fileID: 10905, guid: 0000000000000000f000000000000000, type: 0}
m_Type: 1
m_PreserveAspect: 0
m_FillCenter: 1
m_FillMethod: 4
m_FillAmount: 1
m_FillClockwise: 1
m_FillOrigin: 0
m_UseSpriteMesh: 0
m_PixelsPerUnitMultiplier: 1
--- !u!114 &4979337629572737255
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 8568909860233813620}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 4e29b1a8efbd4b44bb3f3716e73f07ff, type: 3}
m_Name:
m_EditorClassIdentifier:
m_Navigation:
m_Mode: 3
m_WrapAround: 0
m_SelectOnUp: {fileID: 0}
m_SelectOnDown: {fileID: 0}
m_SelectOnLeft: {fileID: 0}
m_SelectOnRight: {fileID: 0}
m_Transition: 1
m_Colors:
m_NormalColor: {r: 1, g: 1, b: 1, a: 1}
m_HighlightedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1}
m_PressedColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 1}
m_SelectedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1}
m_DisabledColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 0.5019608}
m_ColorMultiplier: 1
m_FadeDuration: 0.1
m_SpriteState:
m_HighlightedSprite: {fileID: 0}
m_PressedSprite: {fileID: 0}
m_SelectedSprite: {fileID: 0}
m_DisabledSprite: {fileID: 0}
m_AnimationTriggers:
m_NormalTrigger: Normal
m_HighlightedTrigger: Highlighted
m_PressedTrigger: Pressed
m_SelectedTrigger: Selected
m_DisabledTrigger: Disabled
m_Interactable: 1
m_TargetGraphic: {fileID: 4272345749132423406}
m_OnClick:
m_PersistentCalls:
m_Calls: []
+201
View File
@@ -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<TextAsset> 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<TextAsset>(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<GameObject> 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<GameObject>(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<AudioClip> 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<AudioClip>(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<GameObject> 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.
+74
View File
@@ -0,0 +1,74 @@
# Pooling Manager Execution Flow
## Purpose
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 prefab reference for one Addressables key.
- `PoolManager.cs`: singleton service used by gameplay, server packet handlers, UI code, and effects systems.
## Spawn Flow
1. Call `PoolManager.Instance.SpawnAsync(...)` or `PoolManager.Instance.Spawn(...)`.
2. `PoolManager` finds or creates an `ObjectPool` for the Addressables key.
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 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.
9. The spawned `GameObject` is returned to the caller.
## Despawn Flow
1. Call `PoolManager.Instance.Despawn(gameObject)` or `PoolManager.Instance.Despawn(addressableKey, gameObject)`.
2. The target pool validates that the object is currently active.
3. All `IPoolable.OnDespawn()` hooks are called.
4. The object is deactivated, parented under the pool root, and pushed into the idle stack.
5. When the active count reaches zero, the pool starts the memory release countdown.
## 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 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;
- 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
GameObject fx = await PoolManager.Instance.SpawnAsync(
"effects/fireball.prefab",
hitPosition,
Quaternion.identity,
memoryReleaseTTL: 15f,
autoDespawnTime: 2f);
```
For coroutine-based callers:
```csharp
PoolManager.Instance.Spawn(
"effects/fireball.prefab",
hitPosition,
Quaternion.identity,
memoryReleaseTTL: 15f,
autoDespawnTime: 2f,
onComplete: spawned => { /* use spawned */ });
```