diff --git a/Runtime/ABase/GameObjectPool.meta b/Runtime/ABase/GameObjectPool.meta new file mode 100644 index 0000000..55fb2d8 --- /dev/null +++ b/Runtime/ABase/GameObjectPool.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 3f172362360c0cb4db92f112a02e959a +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/ABase/GameObjectPool/GameObjectPool.cs b/Runtime/ABase/GameObjectPool/GameObjectPool.cs new file mode 100644 index 0000000..8c599b8 --- /dev/null +++ b/Runtime/ABase/GameObjectPool/GameObjectPool.cs @@ -0,0 +1,1216 @@ +#region Class Documentation + +/************************************************************************************************************ +Class Name: GameObjectPool.cs +Type: Pool, GameObject, GameObjectPool + +Example: + // 异步加载游戏物体。 + var gameObject = await GameObjectPool.Instance.GetGameObjectAsync(path, token); + + // 同步加载游戏物体。 + var gameObject = GameObjectPool.Instance.GetGameObject(path); + +Example1: + // 异步加载游戏物体。 + var gameObject = await GameObjectPoolHelper.LoadGameObjectAsync(path, token); + + // 同步加载游戏物体。 + var gameObject = GameObjectPoolHelper.LoadGameObject(path); +************************************************************************************************************/ + +#endregion + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using AlicizaX; +using AlicizaX.Resource.Runtime; +using Cysharp.Threading.Tasks; +using UnityEngine; + + +namespace AlicizaX +{ + + /// + /// 对象池配置项。 + /// + [Serializable] + public class PoolConfig + { + public string asset; + public float time; + public int poolCount; + } + + /// + /// 预制体引用计数信息。 + /// + public class PrefabRefInfo + { + public GameObject Prefab; + public int RefCount; + public float LastAccessTime; + public string AssetPath; + + public PrefabRefInfo(GameObject prefab, string assetPath) + { + this.Prefab = prefab; + this.AssetPath = assetPath; + this.RefCount = 0; + this.LastAccessTime = Time.time; + } + + public void AddRef() + { + RefCount++; + LastAccessTime = Time.time; + } + + public void RemoveRef() + { + if (RefCount > 0) // 防止引用计数变成负数 + { + RefCount--; + if (RefCount > 0) + { + LastAccessTime = Time.time; + } + + Log.Info($"RemoveRef: {AssetPath}, refCount: {RefCount}"); + } + else + { + Log.Warning($"尝试减少已经为0的引用计数: {AssetPath}"); + } + } + + public bool CanUnload(float expireTime) + { + return RefCount <= 0 && expireTime > 0 && (Time.time - LastAccessTime) > expireTime; + } + } + + [Serializable] + public class PooledObject + { + public GameObject gameObject; + public string assetPath; + public float lastUsedTime; + public bool isActive; + public string instanceName; + public bool isRefCountReduced; + + public PooledObject(GameObject go, string path) + { + gameObject = go; + assetPath = path; + lastUsedTime = Time.time; + isActive = false; + instanceName = go.name; + isRefCountReduced = false; + } + + /// + /// 获取过期进度 (0-1),1表示即将过期。 + /// + public float GetExpireProgress(float expireTime) + { + if (expireTime <= 0 || isActive) return 0f; + float timeElapsed = Time.time - lastUsedTime; + return Mathf.Clamp01(timeElapsed / expireTime); + } + + /// + /// 获取剩余时间。 + /// + public float GetRemainingTime(float expireTime) + { + if (expireTime <= 0 || isActive) return -1f; + float timeElapsed = Time.time - lastUsedTime; + return Mathf.Max(0f, expireTime - timeElapsed); + } + } + + /// + /// Inspector显示用的对象信息。 + /// + [Serializable] + public class PoolObjectInfo + { + [SerializeField] public string objectName; + [SerializeField] public string assetPath; + [SerializeField] public bool isActive; + [SerializeField] public float lastUsedTime; + [SerializeField] public float remainingTime; + [SerializeField] public float expireProgress; + [SerializeField] public GameObject gameObject; + + public void UpdateFromPooledObject(PooledObject pooledObj, float expireTime) + { + objectName = pooledObj.instanceName; + assetPath = pooledObj.assetPath; + isActive = pooledObj.isActive; + lastUsedTime = pooledObj.lastUsedTime; + remainingTime = pooledObj.GetRemainingTime(expireTime); + expireProgress = pooledObj.GetExpireProgress(expireTime); + gameObject = pooledObj.gameObject; + } + } + + /// + /// Inspector显示用的预制体信息. + /// + [Serializable] + public class PrefabRefInfoDisplay + { + [SerializeField] public string assetPath; + [SerializeField] public int refCount; + [SerializeField] public float lastAccessTime; + [SerializeField] public GameObject prefab; + + public void UpdateFromPrefabRefInfo(PrefabRefInfo info) + { + assetPath = info.AssetPath; + refCount = info.RefCount; + lastAccessTime = info.LastAccessTime; + prefab = info.Prefab; + } + } + + /// + /// Inspector显示用的池信息。 + /// + [Serializable] + public class ConfigPoolInfo + { + [SerializeField] public string configAsset; + [SerializeField] public int maxCount; + [SerializeField] public float expireTime; + [SerializeField] public int totalObjects; + [SerializeField] public int activeObjects; + [SerializeField] public int availableObjects; + [SerializeField] public int loadedPrefabs; + [SerializeField] public List assetPaths = new List(); + [SerializeField] public List objects = new List(); + [SerializeField] public List prefabRefs = new List(); + + public void UpdateFromPool(ConfigPool pool) + { + configAsset = pool.Config.asset; + maxCount = pool.Config.poolCount; + expireTime = pool.Config.time; + totalObjects = pool.AllObjects.Count; + + activeObjects = 0; + foreach (var obj in pool.AllObjects) + { + if (obj.isActive) activeObjects++; + } + + // 计算所有队列中的可用对象总数 + availableObjects = 0; + foreach (var queue in pool.AvailableObjectsByPath.Values) + { + availableObjects += queue.Count; + } + loadedPrefabs = pool.LoadedPrefabs.Count; + + assetPaths.Clear(); + assetPaths.AddRange(pool.LoadedPrefabs.Keys); + + objects.Clear(); + int objectIndex = 0; + foreach (var pooledObj in pool.AllObjects) + { + if (pooledObj.gameObject != null) + { + PoolObjectInfo info; + if (objectIndex < objects.Count) + { + info = objects[objectIndex]; + } + else + { + info = new PoolObjectInfo(); + objects.Add(info); + } + + info.UpdateFromPooledObject(pooledObj, pool.Config.time); + objectIndex++; + } + } + + prefabRefs.Clear(); + int prefabIndex = 0; + foreach (var kvp in pool.LoadedPrefabs) + { + PrefabRefInfoDisplay info; + if (prefabIndex < prefabRefs.Count) + { + info = prefabRefs[prefabIndex]; + } + else + { + info = new PrefabRefInfoDisplay(); + prefabRefs.Add(info); + } + + info.UpdateFromPrefabRefInfo(kvp.Value); + prefabIndex++; + } + } + } + + /// + /// 配置组对象池 - 管理一个PoolConfig下的所有资源。 + /// + public class ConfigPool + { + public readonly PoolConfig Config; + // 按资源路径分组的可用对象队列 + public readonly Dictionary> AvailableObjectsByPath; + public readonly HashSet AllObjects; + public readonly Dictionary LoadedPrefabs; + public readonly Dictionary>> PendingRequests; + public readonly HashSet LoadingAssets; + public readonly Transform PoolRoot; + + private readonly IResourceLoader _resourceLoader; + + // GameObject到PooledObject的快速查找字典 + private readonly Dictionary _gameObjectToPooledObject; + + // 重用临时队列,避免重复创建。 + private static Queue _tempQueue = new Queue(); + + // 重用过期对象列表,避免重复创建。 + private static readonly List _expiredObjects = new List(); + private static readonly List _expiredPrefabs = new List(); + + public ConfigPool(PoolConfig config, IResourceLoader resourceLoader) + { + _resourceLoader = resourceLoader; + Config = config; + AvailableObjectsByPath = new Dictionary>(); + AllObjects = new HashSet(); + LoadedPrefabs = new Dictionary(); + PendingRequests = new Dictionary>>(); + LoadingAssets = new HashSet(); + _gameObjectToPooledObject = new Dictionary(); + + // 创建池根节点。 + GameObject poolRootGo = new GameObject($"ConfigPool_{config.asset.Replace('/', '_')}"); + PoolRoot = poolRootGo.transform; + PoolRoot.SetParent(GameObjectPool.Instance.poolContainer); + poolRootGo.SetActive(false); + } + + public bool MatchesAsset(string assetPath) + { + return assetPath.StartsWith(Config.asset); + } + + /// + /// 同步获取对象,如果资源未加载则同步加载。 + /// + public GameObject Get(string assetPath) + { + if (!LoadedPrefabs.ContainsKey(assetPath)) + { + if (LoadingAssets.Contains(assetPath)) + { + Log.Warning($"资源 {assetPath} 正在异步加载中,同步获取可能导致重复加载,建议使用异步方法"); + } + + try + { + GameObject prefab = _resourceLoader.LoadPrefab(assetPath); + if (prefab != null) + { + LoadedPrefabs[assetPath] = new PrefabRefInfo(prefab, assetPath); + Log.Info($"同步加载资源成功: {assetPath}"); + } + else + { + Log.Error($"同步加载资源失败: {assetPath}"); + return null; + } + } + catch (Exception e) + { + Log.Error($"同步加载资源异常: {assetPath}, 错误: {e.Message}"); + return null; + } + } + + return GetInternal(assetPath); + } + + /// + /// 异步获取对象。 + /// + public async UniTask GetAsync(string assetPath, CancellationToken cancellationToken = default) + { + if (LoadedPrefabs.ContainsKey(assetPath)) + { + return GetInternal(assetPath); + } + + if (LoadingAssets.Contains(assetPath)) + { + var completionSource = new UniTaskCompletionSource(); + if (!PendingRequests.ContainsKey(assetPath)) + { + PendingRequests[assetPath] = new List>(); + } + + PendingRequests[assetPath].Add(completionSource); + + try + { + return await completionSource.Task.AttachExternalCancellation(cancellationToken); + } + catch (OperationCanceledException) + { + PendingRequests[assetPath].Remove(completionSource); + throw; + } + } + + LoadingAssets.Add(assetPath); + try + { + GameObject prefab = await _resourceLoader.LoadPrefabAsync(assetPath, cancellationToken); + if (prefab != null) + { + LoadedPrefabs[assetPath] = new PrefabRefInfo(prefab, assetPath); + Log.Info($"异步加载资源成功: {assetPath}"); + + if (PendingRequests.ContainsKey(assetPath)) + { + var requests = PendingRequests[assetPath]; + PendingRequests.Remove(assetPath); + + foreach (var request in requests) + { + try + { + var go = GetInternal(assetPath); + request.TrySetResult(go); + } + catch (Exception e) + { + request.TrySetException(e); + } + } + } + + return GetInternal(assetPath); + } + else + { + throw new Exception($"无法异步加载资源: {assetPath}"); + } + } + catch (Exception e) + { + Log.Error($"异步加载资源失败: {assetPath}, 错误: {e.Message}"); + + if (PendingRequests.ContainsKey(assetPath)) + { + var requests = PendingRequests[assetPath]; + PendingRequests.Remove(assetPath); + + foreach (var request in requests) + { + request.TrySetException(e); + } + } + + throw; + } + finally + { + LoadingAssets.Remove(assetPath); + } + } + + /// + /// 创建新的池对象。 + /// + private PooledObject CreatePooledObject(string assetPath) + { + var prefabRefInfo = LoadedPrefabs[assetPath]; + GameObject instantiate = GameObject.Instantiate(prefabRefInfo.Prefab); + var pooledObj = new PooledObject(instantiate, assetPath); + AllObjects.Add(pooledObj); + _gameObjectToPooledObject[instantiate] = pooledObj; + + prefabRefInfo.AddRef(); + + var monitor = instantiate.GetComponent(); + if (monitor == null) + { + monitor = instantiate.AddComponent(); + } + + monitor.Initialize(this, pooledObj); + return pooledObj; + } + + private GameObject GetInternal(string assetPath) + { + PooledObject pooledObj = null; + + // 从按路径分组的队列中获取 + if (AvailableObjectsByPath.TryGetValue(assetPath, out var queue) && queue.Count > 0) + { + // 清理已销毁的对象 + while (queue.Count > 0) + { + var obj = queue.Dequeue(); + if (obj.gameObject == null) + { + // 只有在引用计数未减少时才处理 + if (!obj.isRefCountReduced) + { + OnObjectReallyDestroyed(obj); + } + else + { + // 只需要从集合中移除,不需要减少引用计数 + AllObjects.Remove(obj); + } + continue; + } + + pooledObj = obj; + break; + } + } + + if (pooledObj == null) + { + if (AllObjects.Count < Config.poolCount) + { + pooledObj = CreatePooledObject(assetPath); + } + else + { + // 使用LINQ找到最旧的未使用对象(更高效) + var oldestObj = AllObjects + .Where(obj => !obj.isActive) + .OrderBy(obj => obj.lastUsedTime) + .FirstOrDefault(); + + if (oldestObj != null) + { + DestroyPooledObject(oldestObj); + pooledObj = CreatePooledObject(assetPath); + } + else + { + Log.Warning($"对象池已满且所有对象都在使用中: {Config.asset},无法创建新对象 {assetPath}"); + return null; + } + } + } + + pooledObj.isActive = true; + pooledObj.lastUsedTime = Time.time; + pooledObj.gameObject.SetActive(true); + + return pooledObj.gameObject; + } + + public void Return(GameObject go) + { + if (!_gameObjectToPooledObject.TryGetValue(go, out var pooledObj)) + { + return; + } + + if (pooledObj != null && pooledObj.isActive) + { + pooledObj.isActive = false; + pooledObj.lastUsedTime = Time.time; + + go.SetActive(false); + go.transform.SetParent(PoolRoot); + go.transform.localPosition = Vector3.zero; + go.transform.localRotation = Quaternion.identity; + go.transform.localScale = Vector3.one; + + // 放入按路径分组的队列 + if (!AvailableObjectsByPath.TryGetValue(pooledObj.assetPath, out var queue)) + { + queue = new Queue(); + AvailableObjectsByPath[pooledObj.assetPath] = queue; + } + queue.Enqueue(pooledObj); + } + } + + public void OnObjectDestroyed(PooledObject pooledObj) + { + // 从GameObjectPool的字典中移除,防止内存泄漏 + if (pooledObj.gameObject != null) + { + GameObjectPool.Instance.RemoveGameObjectReference(pooledObj.gameObject); + } + + // 防止重复减少引用计数 + if (!pooledObj.isRefCountReduced) + { + OnObjectReallyDestroyed(pooledObj); + } + else + { + // 只需要从集合中移除 + AllObjects.Remove(pooledObj); + CleanAvailableQueue(pooledObj); + } + } + + private void OnObjectReallyDestroyed(PooledObject pooledObj) + { + // 标记引用计数已减少,防止重复处理 + if (pooledObj.isRefCountReduced) + { + return; + } + + pooledObj.isRefCountReduced = true; + AllObjects.Remove(pooledObj); + + // 从快速查找字典中移除 + if (pooledObj.gameObject != null) + { + _gameObjectToPooledObject.Remove(pooledObj.gameObject); + } + + // 减少预制体引用计数 + if (LoadedPrefabs.TryGetValue(pooledObj.assetPath, out PrefabRefInfo refInfo)) + { + refInfo.RemoveRef(); + } + + CleanAvailableQueue(pooledObj); + } + + // 清理可用队列 + private void CleanAvailableQueue(PooledObject pooledObj) + { + // 从对应路径的队列中移除 + if (AvailableObjectsByPath.TryGetValue(pooledObj.assetPath, out var queue)) + { + _tempQueue.Clear(); + while (queue.Count > 0) + { + var obj = queue.Dequeue(); + if (obj != pooledObj) + { + _tempQueue.Enqueue(obj); + } + } + + // 交换队列 + (queue, _tempQueue) = (_tempQueue, queue); + + // 如果队列为空,从字典中移除 + if (queue.Count == 0) + { + AvailableObjectsByPath.Remove(pooledObj.assetPath); + } + } + } + + private void DestroyPooledObject(PooledObject pooledObj) + { + // 先标记引用计数已减少 + if (pooledObj.isRefCountReduced) + { + return; + } + + // 先处理引用计数 + OnObjectReallyDestroyed(pooledObj); + + if (pooledObj.gameObject != null) + { + GameObject.Destroy(pooledObj.gameObject); + } + } + + public void CheckExpiredObjects() + { + if (Config.time <= 0) return; + + float currentTime = Time.time; + + // 重用过期对象列表 + _expiredObjects.Clear(); + + foreach (var obj in AllObjects) + { + if (!obj.isActive && !obj.isRefCountReduced && (currentTime - obj.lastUsedTime) > Config.time) + { + _expiredObjects.Add(obj); + } + } + + foreach (var expiredObj in _expiredObjects) + { + DestroyPooledObject(expiredObj); + } + + // 重建所有路径的可用队列 + foreach (var kvp in AvailableObjectsByPath.ToList()) + { + var assetPath = kvp.Key; + var queue = kvp.Value; + + _tempQueue.Clear(); + while (queue.Count > 0) + { + var obj = queue.Dequeue(); + if (AllObjects.Contains(obj) && !obj.isRefCountReduced) + { + _tempQueue.Enqueue(obj); + } + } + + if (_tempQueue.Count > 0) + { + // 交换队列 + (queue, _tempQueue) = (_tempQueue, queue); + } + else + { + // 队列为空,从字典中移除 + AvailableObjectsByPath.Remove(assetPath); + } + } + + CheckExpiredPrefabs(); + } + + private void CheckExpiredPrefabs() + { + if (Config.time <= 0) return; + + // 重用过期预制体列表 + _expiredPrefabs.Clear(); + + foreach (var kvp in LoadedPrefabs) + { + var refInfo = kvp.Value; + if (refInfo.CanUnload(Config.time)) + { + _expiredPrefabs.Add(kvp.Key); + } + } + + foreach (var assetPath in _expiredPrefabs) + { + var refInfo = LoadedPrefabs[assetPath]; + Log.Info($"卸载过期预制体: {assetPath}, 引用计数: {refInfo.RefCount}"); + + _resourceLoader.UnloadAsset(refInfo.Prefab); + LoadedPrefabs.Remove(assetPath); + } + } + + public void Clear() + { + foreach (var obj in AllObjects) + { + if (obj.gameObject != null) + { + GameObject.Destroy(obj.gameObject); + } + } + + AllObjects.Clear(); + AvailableObjectsByPath.Clear(); + _gameObjectToPooledObject.Clear(); + + foreach (var kvp in LoadedPrefabs) + { + var refInfo = kvp.Value; + if (refInfo.Prefab != null) + { + Log.Info($"清理时卸载预制体: {kvp.Key}, 引用计数: {refInfo.RefCount}"); + _resourceLoader.UnloadAsset(refInfo.Prefab); + } + } + + LoadedPrefabs.Clear(); + LoadingAssets.Clear(); + + foreach (var requests in PendingRequests.Values) + { + foreach (var request in requests) + { + request.TrySetCanceled(); + } + } + + PendingRequests.Clear(); + + if (PoolRoot != null) + { + GameObject.Destroy(PoolRoot.gameObject); + } + } + } + + /// + /// 对象销毁监听器。 + /// + public class PoolObjectMonitor : MonoBehaviour + { + private ConfigPool _pool; + private PooledObject _pooledObject; + + public void Initialize(ConfigPool pool, PooledObject pooledObject) + { + _pool = pool; + _pooledObject = pooledObject; + } + + private void OnDestroy() + { + if (_pool != null && _pooledObject != null) + { + _pool.OnObjectDestroyed(_pooledObject); + } + } + } + + /// + /// 游戏对象池管理器。 + /// + public class GameObjectPool : MonoBehaviour + { + private static GameObjectPool _instance; + + public static GameObjectPool Instance + { + get + { + if (_instance == null) + { + GameObject go = new GameObject("[GameObjectPool]"); + _instance = go.AddComponent(); + DontDestroyOnLoad(go); + } + + return _instance; + } + } + + [Header("检查间隔")] public float checkInterval = 10f; + + [Header("资源加载器")] public bool useEngineResourceLoader = true; + + [Header("配置路径")] public string poolConfigPath = "Assets/Bundles/Configs/ScriptableObject/PoolConfig"; + + [Header("Inspector显示设置")] public bool showDetailedInfo = true; + + [Header("池状态信息")] [SerializeField] private List poolInfos = new List(); + + public Transform poolContainer; + internal IResourceLoader _resourceLoader; + + private List _poolConfigs; + private List _configPools; + private Dictionary _gameObjectToPool; + + // 重用预加载对象列表 + private static readonly List _preloadedObjects = new List(); + + private float _lastCleanupTime; + + private void Awake() + { + if (_instance == null) + { + _instance = this; + DontDestroyOnLoad(gameObject); + Initialize(); + } + else if (_instance != this) + { + Destroy(gameObject); + } + } + + private void Initialize() + { + _resourceLoader = useEngineResourceLoader ? new AlicizaResourceLoader() as IResourceLoader : new DefaultResourceLoader() as IResourceLoader; + + GameObject containerGo = new GameObject("PoolContainer"); + poolContainer = containerGo.transform; + poolContainer.SetParent(transform); + + _configPools = new List(); + _gameObjectToPool = new Dictionary(); + + try + { + _poolConfigs = ModuleSystem.GetModule().LoadAsset(poolConfigPath).configs; + _poolConfigs.Sort((a, b) => b.asset.Length.CompareTo(a.asset.Length)); + + foreach (var config in _poolConfigs) + { + var configPool = new ConfigPool(config, _resourceLoader); + _configPools.Add(configPool); + } + } + catch (Exception e) + { + Log.Error($"加载对象池配置失败: {e.Message}"); + _poolConfigs = new List(); + } + + // 初始化清理时间 + _lastCleanupTime = Time.time; + } + + private void Update() + { + if (Time.time - _lastCleanupTime >= checkInterval) + { + PerformCleanup(); + _lastCleanupTime = Time.time; + } + } + + /// + /// 执行对象池清理。 + /// + private void PerformCleanup() + { + if (_configPools == null || _configPools.Count == 0) + { + return; + } + + foreach (var pool in _configPools) + { + pool.CheckExpiredObjects(); + } + } + + /// + /// 手动触发一次清理。 + /// + public void ForceCleanup() + { + PerformCleanup(); + _lastCleanupTime = Time.time; + } + + /// + /// 内部方法:从字典中移除GameObject引用,防止内存泄漏。 + /// + internal void RemoveGameObjectReference(GameObject go) + { + if (go != null) + { + _gameObjectToPool.Remove(go); + } + } + + // Editor专用的刷新。 + private void UpdateInspectorInfo() + { + poolInfos.Clear(); + foreach (var pool in _configPools) + { + var info = new ConfigPoolInfo(); + info.UpdateFromPool(pool); + poolInfos.Add(info); + } + } + + public void SetResourceLoader(IResourceLoader resourceLoader) + { + _resourceLoader = resourceLoader; + } + + public GameObject GetGameObject(string assetPath, Transform parent = null) + { + ConfigPool pool = FindConfigPool(assetPath); + GameObject go = null; + + if (pool != null) + { + go = pool.Get(assetPath); + } + else + { + go = _resourceLoader.LoadGameObject(assetPath, parent); + } + + if (go != null && pool != null) + { + _gameObjectToPool[go] = pool; + go.transform.SetParent(parent); + } + + return go; + } + + public async UniTask GetGameObjectAsync(string assetPath, Transform parent = null, CancellationToken cancellationToken = default) + { + ConfigPool pool = FindConfigPool(assetPath); + GameObject go = null; + + if (pool != null) + { + go = await pool.GetAsync(assetPath, cancellationToken); + } + else + { + go = await _resourceLoader.LoadGameObjectAsync(assetPath, parent, cancellationToken); + } + + if (go != null && pool != null) + { + _gameObjectToPool[go] = pool; + go.transform.SetParent(parent); // 设置父节点 + } + + return go; + } + + public void Release(GameObject go) + { + if (go == null) return; + + if (_gameObjectToPool.TryGetValue(go, out ConfigPool pool)) + { + pool.Return(go); + _gameObjectToPool.Remove(go); + } + else + { + Destroy(go); + } + } + + public async UniTask PreloadAsync(string assetPath, int count = 1, CancellationToken cancellationToken = default) + { + ConfigPool pool = FindConfigPool(assetPath); + if (pool == null) + { + Log.Warning($"资源 {assetPath} 没有对应的池配置,无法预加载"); + return; + } + + // 优化:重用预加载对象列表 + _preloadedObjects.Clear(); + for (int i = 0; i < count; i++) + { + GameObject go = await pool.GetAsync(assetPath, cancellationToken); + if (go != null) + { + _preloadedObjects.Add(go); + } + } + + foreach (var go in _preloadedObjects) + { + pool.Return(go); + _gameObjectToPool.Remove(go); + } + } + + public void Preload(string assetPath, int count = 1) + { + ConfigPool pool = FindConfigPool(assetPath); + if (pool == null) + { + Log.Warning($"资源 {assetPath} 没有对应的池配置,无法预加载"); + return; + } + + // 优化:重用预加载对象列表 + _preloadedObjects.Clear(); + for (int i = 0; i < count; i++) + { + GameObject go = pool.Get(assetPath); + if (go != null) + { + _preloadedObjects.Add(go); + } + } + + foreach (var go in _preloadedObjects) + { + pool.Return(go); + _gameObjectToPool.Remove(go); + } + } + + private ConfigPool FindConfigPool(string assetPath) + { + foreach (var pool in _configPools) + { + if (pool.MatchesAsset(assetPath)) + { + return pool; + } + } + + return null; + } + + /// + /// 手动刷新Inspector信息 + /// + public void RefreshInspectorInfo() + { + UpdateInspectorInfo(); + } + + public void ClearAllPools() + { + foreach (var pool in _configPools) + { + pool.Clear(); + } + + _gameObjectToPool.Clear(); + poolInfos.Clear(); + } + + private void OnDestroy() + { + ClearAllPools(); + } + } + + public interface IResourceLoader + { + GameObject LoadPrefab(string location); + UniTask LoadPrefabAsync(string location, CancellationToken cancellationToken = default); + GameObject LoadGameObject(string location, Transform parent = null); + UniTask LoadGameObjectAsync(string location, Transform parent = null, CancellationToken cancellationToken = default); + void UnloadAsset(GameObject gameObject); + } + + public class DefaultResourceLoader : IResourceLoader + { + public GameObject LoadPrefab(string location) + { + return Resources.Load(location); + } + + public async UniTask LoadPrefabAsync(string location, CancellationToken cancellationToken = default) + { + return await Resources.LoadAsync(location).ToUniTask(cancellationToken: cancellationToken) as GameObject; + } + + public GameObject LoadGameObject(string location, Transform parent = null) + { + var prefab = Resources.Load(location); + if (prefab == null) return null; + + var instance = GameObject.Instantiate(prefab); + if (instance != null && parent != null) + { + instance.transform.SetParent(parent); + } + + return instance; + } + + public async UniTask LoadGameObjectAsync(string location, Transform parent = null, CancellationToken cancellationToken = default) + { + var prefab = await Resources.LoadAsync(location).ToUniTask(cancellationToken: cancellationToken) as GameObject; + if (prefab == null) return null; + + var instance = GameObject.Instantiate(prefab); + if (instance != null && parent != null) + { + instance.transform.SetParent(parent); + } + + return instance; + } + + public void UnloadAsset(GameObject gameObject) + { + Resources.UnloadAsset(gameObject); + } + } + + public class AlicizaResourceLoader : IResourceLoader + { + private IResourceModule _resourceModule; + + private void CheckInit() + { + if (_resourceModule == null) + { + _resourceModule = ModuleSystem.GetModule(); + } + } + + public GameObject LoadPrefab(string location) + { + CheckInit(); + return _resourceModule.LoadAsset(location); + } + + public async UniTask LoadPrefabAsync(string location, CancellationToken cancellationToken = default) + { + CheckInit(); + return await _resourceModule.LoadAssetAsync(location, cancellationToken); + } + + public GameObject LoadGameObject(string location, Transform parent = null) + { + CheckInit(); + return _resourceModule.LoadGameObject(location, parent); + } + + public async UniTask LoadGameObjectAsync(string location, Transform parent = null, CancellationToken cancellationToken = default) + { + CheckInit(); + return await _resourceModule.LoadGameObjectAsync(location, parent, cancellationToken); + } + + public void UnloadAsset(GameObject gameObject) + { + CheckInit(); + _resourceModule.UnloadAsset(gameObject); + } + } + + public static class ObjectPoolUtil + { + public static GameObject LoadGameObject(string assetPath, Transform parent = null) + { + return GameObjectPool.Instance.GetGameObject(assetPath); + } + + public static async UniTask LoadGameObjectAsync(string assetPath, Transform parent = null, CancellationToken cancellationToken = default) + { + return await GameObjectPool.Instance.GetGameObjectAsync(assetPath, parent, cancellationToken); + } + + public static void Release(GameObject go) + { + GameObjectPool.Instance.Release(go); + } + } +} + + diff --git a/Runtime/ABase/GameObjectPool/GameObjectPool.cs.meta b/Runtime/ABase/GameObjectPool/GameObjectPool.cs.meta new file mode 100644 index 0000000..79c907a --- /dev/null +++ b/Runtime/ABase/GameObjectPool/GameObjectPool.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: ce0e8ead006ba324eaf2410a3dd556a5 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/ABase/GameObjectPool/GameObjectPoolEditor.cs b/Runtime/ABase/GameObjectPool/GameObjectPoolEditor.cs new file mode 100644 index 0000000..9e69cc4 --- /dev/null +++ b/Runtime/ABase/GameObjectPool/GameObjectPoolEditor.cs @@ -0,0 +1,322 @@ +using UnityEditor; +using UnityEngine; + +#if UNITY_EDITOR +namespace AlicizaX +{ + [CustomEditor(typeof(GameObjectPool))] + public class GameObjectPoolEditor : UnityEditor.Editor + { + private bool[] _poolFoldouts; + private bool[] _prefabFoldouts; + private float _lastRefreshTime; + private const float AUTO_REFRESH_INTERVAL = 0.1f; + + // 缓存序列化属性,避免重复查找 + private SerializedProperty _poolInfosProperty; + + private void OnEnable() + { + _poolInfosProperty = serializedObject.FindProperty("poolInfos"); + _lastRefreshTime = Time.time; + } + + public override void OnInspectorGUI() + { + var pool = (GameObjectPool)target; + + // 更新序列化对象 + serializedObject.Update(); + + // 绘制默认Inspector + DrawDefaultInspector(); + EditorGUILayout.Space(); + + // 手动刷新按钮 + if (GUILayout.Button("刷新池状态信息")) + { + RefreshPoolInfo(pool); + } + + // 检查是否需要自动刷新 + bool shouldAutoRefresh = EditorApplication.isPlaying && pool.showDetailedInfo && + Selection.activeGameObject == pool.gameObject && + Time.time - _lastRefreshTime > AUTO_REFRESH_INTERVAL; + + if (shouldAutoRefresh) + { + RefreshPoolInfo(pool); + } + + if (!pool.showDetailedInfo) + { + serializedObject.ApplyModifiedProperties(); + return; + } + + EditorGUILayout.Space(); + EditorGUILayout.LabelField("对象池详细信息", EditorStyles.boldLabel); + + // 重新获取属性以确保数据是最新的 + _poolInfosProperty = serializedObject.FindProperty("poolInfos"); + + if (_poolInfosProperty != null && _poolInfosProperty.arraySize > 0) + { + DrawPoolInfos(); + } + else + { + EditorGUILayout.HelpBox("暂无池信息,请等待系统初始化或点击刷新按钮", MessageType.Info); + } + + // 显示自动刷新状态 + if (Selection.activeGameObject == pool.gameObject) + { + EditorGUILayout.HelpBox("Inspector正在自动刷新 (仅在选中时)", MessageType.Info); + } + + // 应用修改的属性 + serializedObject.ApplyModifiedProperties(); + } + + private void RefreshPoolInfo(GameObjectPool pool) + { + pool.RefreshInspectorInfo(); + _lastRefreshTime = Time.time; + serializedObject.Update(); // 立即更新序列化对象 + + // 标记需要重绘 + if (Selection.activeGameObject == pool.gameObject) + { + EditorUtility.SetDirty(pool); + Repaint(); + } + } + + private void DrawPoolInfos() + { + int poolCount = _poolInfosProperty.arraySize; + + // 确保折叠状态数组大小正确 + if (_poolFoldouts == null || _poolFoldouts.Length != poolCount) + { + bool[] oldPoolFoldouts = _poolFoldouts; + bool[] oldPrefabFoldouts = _prefabFoldouts; + + _poolFoldouts = new bool[poolCount]; + _prefabFoldouts = new bool[poolCount]; + + // 保持之前的折叠状态 + if (oldPoolFoldouts != null) + { + for (int i = 0; i < Mathf.Min(oldPoolFoldouts.Length, poolCount); i++) + { + _poolFoldouts[i] = oldPoolFoldouts[i]; + if (oldPrefabFoldouts != null && i < oldPrefabFoldouts.Length) + { + _prefabFoldouts[i] = oldPrefabFoldouts[i]; + } + } + } + } + + for (int i = 0; i < poolCount; i++) + { + DrawPoolInfo(i); + } + } + + private void DrawPoolInfo(int poolIndex) + { + var poolInfo = _poolInfosProperty.GetArrayElementAtIndex(poolIndex); + if (poolInfo == null) return; + + var configAssetProp = poolInfo.FindPropertyRelative("configAsset"); + var totalObjectsProp = poolInfo.FindPropertyRelative("totalObjects"); + var maxCountProp = poolInfo.FindPropertyRelative("maxCount"); + var activeObjectsProp = poolInfo.FindPropertyRelative("activeObjects"); + + if (configAssetProp == null || totalObjectsProp == null || maxCountProp == null || activeObjectsProp == null) + return; + + string configAsset = configAssetProp.stringValue; + int totalObjects = totalObjectsProp.intValue; + int maxCount = maxCountProp.intValue; + int activeObjects = activeObjectsProp.intValue; + + EditorGUILayout.BeginVertical("box"); + + // 使用Rect布局来精确控制Foldout的大小 + Rect rect = EditorGUILayout.GetControlRect(); + Rect foldoutRect = new Rect(rect.x, rect.y, 15, rect.height); + Rect progressRect = new Rect(rect.x + 20, rect.y, rect.width - 120, rect.height); + Rect labelRect = new Rect(rect.x + rect.width - 95, rect.y, 95, rect.height); + + // 绘制折叠按钮 + _poolFoldouts[poolIndex] = EditorGUI.Foldout(foldoutRect, _poolFoldouts[poolIndex], GUIContent.none); + + // 使用率进度条 + float usage = maxCount > 0 ? (float)totalObjects / maxCount : 0f; + EditorGUI.ProgressBar(progressRect, usage, $"{configAsset} ({totalObjects}/{maxCount})"); + + // 活跃对象数 + EditorGUI.LabelField(labelRect, $"活跃:{activeObjects}", EditorStyles.miniLabel); + + if (_poolFoldouts[poolIndex]) + { + EditorGUI.indentLevel++; + DrawPoolDetails(poolInfo, poolIndex); + EditorGUI.indentLevel--; + } + + EditorGUILayout.EndVertical(); + EditorGUILayout.Space(); + } + + private void DrawPoolDetails(SerializedProperty poolInfo, int poolIndex) + { + var configAssetProp = poolInfo.FindPropertyRelative("configAsset"); + var maxCountProp = poolInfo.FindPropertyRelative("maxCount"); + var expireTimeProp = poolInfo.FindPropertyRelative("expireTime"); + var loadedPrefabsProp = poolInfo.FindPropertyRelative("loadedPrefabs"); + + if (configAssetProp != null) + EditorGUILayout.LabelField($"配置路径: {configAssetProp.stringValue}"); + if (maxCountProp != null) + EditorGUILayout.LabelField($"最大数量: {maxCountProp.intValue}"); + if (expireTimeProp != null) + EditorGUILayout.LabelField($"过期时间: {expireTimeProp.floatValue}s"); + if (loadedPrefabsProp != null) + EditorGUILayout.LabelField($"已加载预制体: {loadedPrefabsProp.intValue}"); + + EditorGUILayout.Space(); + + // 绘制预制体引用信息 + DrawPrefabRefs(poolInfo, poolIndex); + + // 绘制对象详细信息 + DrawObjectDetails(poolInfo); + } + + private void DrawPrefabRefs(SerializedProperty poolInfo, int poolIndex) + { + var prefabRefsProp = poolInfo.FindPropertyRelative("prefabRefs"); + if (prefabRefsProp == null || prefabRefsProp.arraySize <= 0) return; + + // 使用简单的Foldout,不指定宽度 + _prefabFoldouts[poolIndex] = EditorGUILayout.Foldout(_prefabFoldouts[poolIndex], "预制体引用信息:"); + + if (_prefabFoldouts[poolIndex]) + { + EditorGUI.indentLevel++; + + for (int j = 0; j < prefabRefsProp.arraySize; j++) + { + DrawPrefabRefInfo(prefabRefsProp.GetArrayElementAtIndex(j)); + } + + EditorGUI.indentLevel--; + } + + EditorGUILayout.Space(); + } + + private void DrawPrefabRefInfo(SerializedProperty prefabRef) + { + if (prefabRef == null) return; + + var assetPathProp = prefabRef.FindPropertyRelative("assetPath"); + var refCountProp = prefabRef.FindPropertyRelative("refCount"); + var lastAccessTimeProp = prefabRef.FindPropertyRelative("lastAccessTime"); + var prefabObjProp = prefabRef.FindPropertyRelative("prefab"); + + EditorGUILayout.BeginHorizontal("box"); + + EditorGUILayout.BeginVertical(); + if (assetPathProp != null) + EditorGUILayout.LabelField($"{System.IO.Path.GetFileName(assetPathProp.stringValue)}", EditorStyles.boldLabel); + if (refCountProp != null) + EditorGUILayout.LabelField($"引用计数: {refCountProp.intValue}", EditorStyles.miniLabel); + if (lastAccessTimeProp != null) + EditorGUILayout.LabelField($"最后访问: {(Time.time - lastAccessTimeProp.floatValue):F1}秒前", EditorStyles.miniLabel); + EditorGUILayout.EndVertical(); + + if (prefabObjProp != null) + EditorGUILayout.ObjectField(prefabObjProp.objectReferenceValue, typeof(GameObject), false, GUILayout.Width(100)); + + EditorGUILayout.EndHorizontal(); + } + + private void DrawObjectDetails(SerializedProperty poolInfo) + { + var objectsProp = poolInfo.FindPropertyRelative("objects"); + if (objectsProp == null || objectsProp.arraySize <= 0) return; + + EditorGUILayout.LabelField("对象详情:", EditorStyles.boldLabel); + + for (int j = 0; j < objectsProp.arraySize; j++) + { + DrawObjectInfo(objectsProp.GetArrayElementAtIndex(j)); + } + } + + private void DrawObjectInfo(SerializedProperty obj) + { + if (obj == null) return; + + var objNameProp = obj.FindPropertyRelative("objectName"); + var objAssetPathProp = obj.FindPropertyRelative("assetPath"); + var isActiveProp = obj.FindPropertyRelative("isActive"); + var remainingTimeProp = obj.FindPropertyRelative("remainingTime"); + var expireProgressProp = obj.FindPropertyRelative("expireProgress"); + var gameObjectProp = obj.FindPropertyRelative("gameObject"); + + EditorGUILayout.BeginHorizontal("box"); + + // 状态颜色指示器 + bool isActive = isActiveProp?.boolValue ?? false; + var statusColor = isActive ? Color.green : Color.yellow; + var prevColor = GUI.color; + GUI.color = statusColor; + EditorGUILayout.LabelField("●", GUILayout.Width(15)); + GUI.color = prevColor; + + EditorGUILayout.BeginVertical(); + + // 对象名称和路径 + string objName = objNameProp?.stringValue ?? "Unknown"; + string objAssetPath = objAssetPathProp?.stringValue ?? ""; + EditorGUILayout.LabelField($"{objName} ({System.IO.Path.GetFileName(objAssetPath)})", EditorStyles.boldLabel); + EditorGUILayout.LabelField($"状态: {(isActive ? "活跃" : "空闲")}", EditorStyles.miniLabel); + + // 过期进度条 + if (!isActive && remainingTimeProp != null && expireProgressProp != null) + { + float remainingTime = remainingTimeProp.floatValue; + float expireProgress = expireProgressProp.floatValue; + + if (remainingTime >= 0) + { + Rect expireRect = GUILayoutUtility.GetRect(100, 16, GUILayout.ExpandWidth(true), GUILayout.Height(16)); + EditorGUI.ProgressBar(expireRect, expireProgress, $"释放倒计时: {remainingTime:F1}s"); + } + } + + EditorGUILayout.EndVertical(); + + // GameObject引用 + if (gameObjectProp != null) + EditorGUILayout.ObjectField(gameObjectProp.objectReferenceValue, typeof(GameObject), true, GUILayout.Width(100)); + + EditorGUILayout.EndHorizontal(); + } + + public override bool RequiresConstantRepaint() + { + // 只有在选中对象池时才需要持续重绘 + var pool = target as GameObjectPool; + return pool != null && pool.showDetailedInfo && Selection.activeGameObject == pool.gameObject; + } + } +} +#endif diff --git a/Runtime/ABase/GameObjectPool/GameObjectPoolEditor.cs.meta b/Runtime/ABase/GameObjectPool/GameObjectPoolEditor.cs.meta new file mode 100644 index 0000000..247e064 --- /dev/null +++ b/Runtime/ABase/GameObjectPool/GameObjectPoolEditor.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: df436879f8854a95b5a92e8b77772189 +timeCreated: 1773109368 \ No newline at end of file diff --git a/Runtime/ABase/GameObjectPool/PoolConfigScriptableObject.cs b/Runtime/ABase/GameObjectPool/PoolConfigScriptableObject.cs new file mode 100644 index 0000000..7a00c9d --- /dev/null +++ b/Runtime/ABase/GameObjectPool/PoolConfigScriptableObject.cs @@ -0,0 +1,12 @@ +using System.Collections.Generic; +using UnityEngine; + +namespace AlicizaX +{ + [CreateAssetMenu(fileName = "PoolConfig", menuName = "GameplaySystem/PoolConfig", order = 10)] + public class PoolConfigScriptableObject : ScriptableObject + { + public List configs; + } + +} diff --git a/Runtime/ABase/GameObjectPool/PoolConfigScriptableObject.cs.meta b/Runtime/ABase/GameObjectPool/PoolConfigScriptableObject.cs.meta new file mode 100644 index 0000000..b910757 --- /dev/null +++ b/Runtime/ABase/GameObjectPool/PoolConfigScriptableObject.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 32739bac255eb5f428628746c6e427f4 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Resource/Resource/ResourceModule.cs b/Runtime/Resource/Resource/ResourceModule.cs index 5a36ef2..624148f 100644 --- a/Runtime/Resource/Resource/ResourceModule.cs +++ b/Runtime/Resource/Resource/ResourceModule.cs @@ -644,11 +644,6 @@ namespace AlicizaX.Resource.Runtime return; } - if (string.IsNullOrEmpty(location)) - { - throw new GameFrameworkException("Asset name is invalid."); - } - if (!CheckLocationValid(location, packageName)) { Log.Error($"Could not found location [{location}].");