This commit is contained in:
陈思海 2026-04-17 21:01:20 +08:00
parent 9d75547998
commit c4c9a22fc4
14 changed files with 2767 additions and 516 deletions

View File

@ -80,13 +80,26 @@ namespace AlicizaX
if (_foldoutState[key])
{
EditorGUILayout.LabelField("Match Mode", snapshot.matchMode.ToString());
EditorGUILayout.LabelField("Entry", snapshot.entryName);
EditorGUILayout.LabelField("Loader", snapshot.loaderType.ToString());
EditorGUILayout.LabelField("Capacity", snapshot.capacity.ToString());
EditorGUILayout.LabelField("Overflow Policy", snapshot.overflowPolicy.ToString());
EditorGUILayout.LabelField("Min Retained", snapshot.minRetained.ToString());
EditorGUILayout.LabelField("Soft Capacity", snapshot.softCapacity.ToString());
EditorGUILayout.LabelField("Hard Capacity", snapshot.hardCapacity.ToString());
EditorGUILayout.LabelField("Inactive", snapshot.inactiveCount.ToString());
EditorGUILayout.LabelField("Idle Timeout", $"{snapshot.instanceIdleTimeout:F1}s");
EditorGUILayout.LabelField("Prefab Unload Delay", $"{snapshot.prefabUnloadDelay:F1}s");
EditorGUILayout.LabelField("Prefab Loaded", snapshot.prefabLoaded ? "Yes" : "No");
EditorGUILayout.LabelField("Prefab Idle", $"{snapshot.prefabIdleDuration:F1}s");
EditorGUILayout.Space();
EditorGUILayout.LabelField("Acquire", snapshot.acquireCount.ToString());
EditorGUILayout.LabelField("Release", snapshot.releaseCount.ToString());
EditorGUILayout.LabelField("Hit", snapshot.hitCount.ToString());
EditorGUILayout.LabelField("Miss", snapshot.missCount.ToString());
EditorGUILayout.LabelField("Expand", snapshot.expandCount.ToString());
EditorGUILayout.LabelField("Exhausted", snapshot.exhaustedCount.ToString());
EditorGUILayout.LabelField("Auto Recycle", snapshot.autoRecycleCount.ToString());
EditorGUILayout.LabelField("Destroy", snapshot.destroyCount.ToString());
EditorGUILayout.LabelField("Peak Active", snapshot.peakActive.ToString());
EditorGUILayout.LabelField("Peak Total", snapshot.peakTotal.ToString());
if (snapshot.instances.Count > 0)
{
@ -114,6 +127,7 @@ namespace AlicizaX
EditorGUILayout.BeginVertical();
EditorGUILayout.LabelField(instance.instanceName, EditorStyles.boldLabel);
EditorGUILayout.LabelField("State", instance.isActive ? "Active" : "Inactive");
EditorGUILayout.LabelField("Life", $"{instance.lifeDuration:F1}s");
if (!instance.isActive)
{
EditorGUILayout.LabelField("Idle", $"{instance.idleDuration:F1}s");

View File

@ -0,0 +1,596 @@
using System.Collections.Generic;
using AlicizaX.Editor;
using UnityEditor;
using UnityEditor.Callbacks;
using UnityEditor.UIElements;
using UnityEngine;
using UnityEngine.UIElements;
namespace AlicizaX
{
public sealed class PoolConfigEditorWindow : EditorWindow
{
private const float MinLeftWidth = 240f;
private const float InitialLeftWidth = 300f;
private const int ListItemHeight = 46;
private const string WindowTitle = "Pool Config Editor";
private static readonly Color LeftPanelColor = new Color(0.22f, 0.22f, 0.22f, 1f);
private static readonly Color RightPanelColor = new Color(0.16f, 0.16f, 0.16f, 1f);
private PoolConfigScriptableObject _asset;
private SerializedObject _serializedObject;
private SerializedProperty _entriesProperty;
private readonly List<int> _entryIndices = new List<int>();
private int _selectedIndex;
private bool _hasUnsavedChanges;
private ToolbarButton _saveButton;
private Label _titleLabel;
private VisualElement _leftPane;
private ListView _listView;
private ScrollView _detailScrollView;
private Label _detailTitleLabel;
private VisualElement _detailFieldsContainer;
private VisualElement _emptyContainer;
[MenuItem("AlicizaX/GameObjectPool/Open PoolConfig Editor")]
public static void OpenWindow()
{
Open(Selection.activeObject as PoolConfigScriptableObject);
}
public static void Open(PoolConfigScriptableObject asset)
{
PoolConfigEditorWindow window = GetWindow<PoolConfigEditorWindow>(false, WindowTitle, true);
window.minSize = new Vector2(920f, 560f);
window.SetAsset(asset);
window.Show();
}
[OnOpenAsset(0)]
private static bool OnOpenAsset(int instanceId, int line)
{
Object obj = EditorUtility.InstanceIDToObject(instanceId);
if (obj is not PoolConfigScriptableObject asset)
{
return false;
}
Open(asset);
return true;
}
private void CreateGUI()
{
titleContent = new GUIContent(WindowTitle, EditorGUIUtility.IconContent("ScriptableObject Icon").image);
BuildUi();
if (_asset == null && Selection.activeObject is PoolConfigScriptableObject selectedAsset)
{
SetAsset(selectedAsset);
}
else
{
RefreshUi();
}
}
private void OnEnable()
{
titleContent = new GUIContent(WindowTitle, EditorGUIUtility.IconContent("ScriptableObject Icon").image);
if (rootVisualElement.childCount == 0)
{
BuildUi();
}
if (_asset == null && Selection.activeObject is PoolConfigScriptableObject selectedAsset)
{
SetAsset(selectedAsset);
}
else
{
RefreshUi();
}
}
private void OnSelectionChange()
{
if (Selection.activeObject is PoolConfigScriptableObject selectedAsset && selectedAsset != _asset)
{
SetAsset(selectedAsset);
}
}
private void BuildUi()
{
rootVisualElement.Clear();
rootVisualElement.style.flexDirection = FlexDirection.Column;
Toolbar toolbar = new Toolbar();
toolbar.style.flexShrink = 0f;
_saveButton = new ToolbarButton(SaveAsset)
{
tooltip = "Save PoolConfig"
};
_saveButton.Add(new Image
{
image = EditorGUIUtility.IconContent("SaveActive").image,
scaleMode = ScaleMode.ScaleToFit
});
_titleLabel = new Label();
_titleLabel.style.unityFontStyleAndWeight = FontStyle.Bold;
_titleLabel.style.flexGrow = 1f;
_titleLabel.style.marginLeft = 6f;
_titleLabel.style.unityTextAlign = TextAnchor.MiddleLeft;
toolbar.Add(_saveButton);
toolbar.Add(_titleLabel);
rootVisualElement.Add(toolbar);
TwoPaneSplitView splitView = new TwoPaneSplitView(0, InitialLeftWidth, TwoPaneSplitViewOrientation.Horizontal);
splitView.style.flexGrow = 1f;
rootVisualElement.Add(splitView);
_leftPane = new VisualElement();
_leftPane.style.flexGrow = 1f;
_leftPane.style.minWidth = MinLeftWidth;
_leftPane.style.backgroundColor = LeftPanelColor;
_leftPane.style.paddingLeft = 4f;
_leftPane.style.paddingRight = 4f;
_leftPane.style.paddingTop = 4f;
_leftPane.style.paddingBottom = 4f;
VisualElement rightPane = new VisualElement();
rightPane.style.flexGrow = 1f;
rightPane.style.backgroundColor = RightPanelColor;
rightPane.style.paddingLeft = 10f;
rightPane.style.paddingRight = 10f;
rightPane.style.paddingTop = 8f;
rightPane.style.paddingBottom = 8f;
splitView.Add(_leftPane);
splitView.Add(rightPane);
BuildLeftPane();
BuildRightPane(rightPane);
}
private void BuildLeftPane()
{
_listView = new ListView
{
selectionType = SelectionType.Single,
virtualizationMethod = CollectionVirtualizationMethod.FixedHeight,
reorderable = false,
showBorder = false,
showAlternatingRowBackgrounds = AlternatingRowBackground.None,
style =
{
flexGrow = 1f,
marginBottom = 4f
},
fixedItemHeight = ListItemHeight
};
_listView.makeItem = MakeListItem;
_listView.bindItem = BindListItem;
_listView.selectionChanged += _ => OnListSelectionChanged();
_leftPane.Add(_listView);
VisualElement footer = new VisualElement();
footer.style.flexDirection = FlexDirection.Row;
footer.style.justifyContent = Justify.Center;
footer.style.alignItems = Align.Center;
footer.style.flexShrink = 0f;
footer.style.height = 28f;
Button addButton = new Button(AddEntry)
{
tooltip = "Add Item"
};
addButton.style.width = 24f;
addButton.style.height = 20f;
addButton.style.marginRight = 4f;
addButton.Add(new Image
{
image = EditorUtils.Styles.PlusIcon.image,
scaleMode = ScaleMode.ScaleToFit
});
Button removeButton = new Button(RemoveEntry)
{
tooltip = "Remove Item",
name = "remove-button"
};
removeButton.style.width = 24f;
removeButton.style.height = 20f;
removeButton.Add(new Image
{
image = EditorUtils.Styles.MinusIcon.image,
scaleMode = ScaleMode.ScaleToFit
});
removeButton.SetEnabled(false);
footer.Add(addButton);
footer.Add(removeButton);
_leftPane.Add(footer);
}
private void BuildRightPane(VisualElement rightPane)
{
_emptyContainer = new VisualElement
{
style =
{
flexGrow = 1f,
justifyContent = Justify.Center
}
};
_emptyContainer.Add(new HelpBox("No entry selected.", HelpBoxMessageType.Info));
_detailScrollView = new ScrollView
{
style =
{
flexGrow = 1f
}
};
_detailTitleLabel = new Label();
_detailTitleLabel.style.unityFontStyleAndWeight = FontStyle.Bold;
_detailTitleLabel.style.marginBottom = 6f;
_detailTitleLabel.style.unityTextAlign = TextAnchor.MiddleLeft;
_detailFieldsContainer = new VisualElement();
_detailFieldsContainer.style.flexDirection = FlexDirection.Column;
_detailFieldsContainer.style.flexGrow = 1f;
_detailScrollView.Add(_detailTitleLabel);
_detailScrollView.Add(_detailFieldsContainer);
rightPane.Add(_emptyContainer);
rightPane.Add(_detailScrollView);
}
private VisualElement MakeListItem()
{
VisualElement root = new VisualElement();
root.style.height = ListItemHeight;
root.style.flexDirection = FlexDirection.Column;
root.style.justifyContent = Justify.Center;
root.style.paddingLeft = 8f;
root.style.paddingRight = 8f;
root.style.paddingTop = 6f;
root.style.paddingBottom = 6f;
root.style.marginBottom = 2f;
Label primary = new Label { name = "primary" };
primary.style.unityTextAlign = TextAnchor.MiddleLeft;
Label secondary = new Label { name = "secondary" };
secondary.style.unityTextAlign = TextAnchor.MiddleLeft;
secondary.style.fontSize = 11f;
secondary.style.color = new Color(0.72f, 0.72f, 0.72f, 1f);
root.Add(primary);
root.Add(secondary);
return root;
}
private void BindListItem(VisualElement element, int index)
{
SerializedProperty entry = GetEntryAt(index);
element.Q<Label>("primary").text = GetPrimaryLabel(entry);
element.Q<Label>("secondary").text = GetSecondaryLabel(entry);
}
private void SetAsset(PoolConfigScriptableObject asset)
{
_asset = asset;
_selectedIndex = 0;
_hasUnsavedChanges = false;
if (_asset == null)
{
_serializedObject = null;
_entriesProperty = null;
}
else
{
_asset.Normalize();
_serializedObject = new SerializedObject(_asset);
_entriesProperty = _serializedObject.FindProperty("entries");
RebuildEntryIndices();
ClampSelection();
}
RefreshUi();
}
private void RefreshUi()
{
if (_titleLabel == null)
{
return;
}
RefreshTitle();
_saveButton?.SetEnabled(_asset != null);
if (_asset == null || _serializedObject == null)
{
_entryIndices.Clear();
_listView.itemsSource = _entryIndices;
_listView.Rebuild();
_emptyContainer.Clear();
_emptyContainer.Add(new HelpBox("Select or double-click a PoolConfig asset to edit it in this window.", HelpBoxMessageType.Info));
_emptyContainer.style.display = DisplayStyle.Flex;
_detailScrollView.style.display = DisplayStyle.None;
UpdateRemoveButtonState();
return;
}
RebuildEntryIndices();
ClampSelection();
_listView.itemsSource = _entryIndices;
_listView.Rebuild();
if (_entryIndices.Count > 0)
{
_listView.SetSelectionWithoutNotify(new[] { _selectedIndex });
_emptyContainer.style.display = DisplayStyle.None;
_detailScrollView.style.display = DisplayStyle.Flex;
RebuildDetailFields();
}
else
{
_emptyContainer.Clear();
_emptyContainer.Add(new HelpBox("No entry selected.", HelpBoxMessageType.Info));
_emptyContainer.style.display = DisplayStyle.Flex;
_detailScrollView.style.display = DisplayStyle.None;
}
UpdateRemoveButtonState();
}
private void RebuildEntryIndices()
{
_entryIndices.Clear();
int count = _entriesProperty?.arraySize ?? 0;
for (int i = 0; i < count; i++)
{
_entryIndices.Add(i);
}
}
private void UpdateRemoveButtonState()
{
Button removeButton = _leftPane?.Q<Button>("remove-button");
removeButton?.SetEnabled(_entryIndices.Count > 0);
}
private void OnListSelectionChanged()
{
if (_listView.selectedIndex < 0 || _listView.selectedIndex >= _entryIndices.Count)
{
return;
}
_selectedIndex = _listView.selectedIndex;
UpdateRemoveButtonState();
RebuildDetailFields();
}
private void RebuildDetailFields()
{
if (_asset == null || _serializedObject == null)
{
return;
}
SerializedProperty selectedProperty = GetSelectedProperty();
if (selectedProperty == null)
{
return;
}
_serializedObject.Update();
_detailTitleLabel.text = GetPrimaryLabel(selectedProperty);
_detailFieldsContainer.Unbind();
_detailFieldsContainer.Clear();
SerializedProperty iterator = selectedProperty.Copy();
SerializedProperty end = iterator.GetEndProperty();
bool enterChildren = true;
while (iterator.NextVisible(enterChildren) && !SerializedProperty.EqualContents(iterator, end))
{
PropertyField field = new PropertyField(iterator.Copy());
field.RegisterCallback<SerializedPropertyChangeEvent>(OnDetailPropertyChanged);
_detailFieldsContainer.Add(field);
enterChildren = false;
}
_detailFieldsContainer.Bind(_serializedObject);
}
private void OnDetailPropertyChanged(SerializedPropertyChangeEvent evt)
{
if (_asset == null || _serializedObject == null)
{
return;
}
_serializedObject.ApplyModifiedPropertiesWithoutUndo();
_asset.Normalize();
_hasUnsavedChanges = true;
_listView?.RefreshItems();
RefreshTitle();
_detailTitleLabel.text = GetPrimaryLabel(GetSelectedProperty());
}
private void AddEntry()
{
if (_entriesProperty == null)
{
return;
}
_serializedObject.Update();
int index = _entriesProperty.arraySize;
_entriesProperty.InsertArrayElementAtIndex(index);
SerializedProperty property = _entriesProperty.GetArrayElementAtIndex(index);
InitializeNewEntry(property, index);
_serializedObject.ApplyModifiedPropertiesWithoutUndo();
_asset.Normalize();
RebuildEntryIndices();
_selectedIndex = index;
_hasUnsavedChanges = true;
RefreshUi();
_listView.SetSelectionWithoutNotify(new[] { _selectedIndex });
}
private void RemoveEntry()
{
if (_entriesProperty == null || _entryIndices.Count == 0)
{
return;
}
_serializedObject.Update();
_entriesProperty.DeleteArrayElementAtIndex(_selectedIndex);
_serializedObject.ApplyModifiedPropertiesWithoutUndo();
_asset.Normalize();
RebuildEntryIndices();
if (_selectedIndex >= _entryIndices.Count)
{
_selectedIndex = Mathf.Max(0, _entryIndices.Count - 1);
}
_hasUnsavedChanges = true;
RefreshUi();
if (_entryIndices.Count > 0)
{
_listView.SetSelectionWithoutNotify(new[] { _selectedIndex });
}
}
private void InitializeNewEntry(SerializedProperty property, int index)
{
property.FindPropertyRelative("entryName").stringValue = $"PoolEntry{index + 1}";
property.FindPropertyRelative("group").stringValue = PoolEntry.DefaultGroup;
property.FindPropertyRelative("assetPath").stringValue = string.Empty;
property.FindPropertyRelative("matchMode").enumValueIndex = (int)PoolMatchMode.Exact;
property.FindPropertyRelative("loaderType").enumValueIndex = (int)PoolResourceLoaderType.AssetBundle;
property.FindPropertyRelative("overflowPolicy").enumValueIndex = (int)PoolOverflowPolicy.FailFast;
property.FindPropertyRelative("trimPolicy").enumValueIndex = (int)PoolTrimPolicy.IdleOnly;
property.FindPropertyRelative("activationMode").enumValueIndex = (int)PoolActivationMode.SetActive;
property.FindPropertyRelative("resetMode").enumValueIndex = (int)PoolResetMode.PoolableCallbacks;
property.FindPropertyRelative("minRetained").intValue = 0;
property.FindPropertyRelative("softCapacity").intValue = 8;
property.FindPropertyRelative("hardCapacity").intValue = 8;
property.FindPropertyRelative("idleTrimDelay").floatValue = 30f;
property.FindPropertyRelative("prefabUnloadDelay").floatValue = 60f;
property.FindPropertyRelative("autoRecycleDelay").floatValue = 0f;
property.FindPropertyRelative("trimBatchPerTick").intValue = 2;
property.FindPropertyRelative("warmupBatchPerFrame").intValue = 2;
property.FindPropertyRelative("warmupFrameBudgetMs").floatValue = 1f;
property.FindPropertyRelative("allowRuntimeExpand").boolValue = false;
property.FindPropertyRelative("keepPrefabResident").boolValue = false;
property.FindPropertyRelative("aggressiveTrimOnLowMemory").boolValue = false;
property.FindPropertyRelative("priority").intValue = index;
}
private void SaveAsset()
{
if (_asset == null || _serializedObject == null)
{
return;
}
_serializedObject.ApplyModifiedProperties();
_asset.Normalize();
EditorUtility.SetDirty(_asset);
AssetDatabase.SaveAssets();
_serializedObject.Update();
_hasUnsavedChanges = false;
RefreshUi();
}
private void RefreshTitle()
{
string assetLabel = _asset == null ? "No PoolConfig Selected" : _asset.name;
if (_hasUnsavedChanges)
{
assetLabel += " *";
}
_titleLabel.text = assetLabel;
}
private SerializedProperty GetEntryAt(int index)
{
return _entriesProperty == null || index < 0 || index >= _entriesProperty.arraySize
? null
: _entriesProperty.GetArrayElementAtIndex(index);
}
private SerializedProperty GetSelectedProperty()
{
if (_entryIndices.Count == 0)
{
return null;
}
ClampSelection();
return GetEntryAt(_selectedIndex);
}
private string GetPrimaryLabel(SerializedProperty property)
{
if (property == null)
{
return "<Missing>";
}
string entryName = property.FindPropertyRelative("entryName").stringValue;
string assetPath = property.FindPropertyRelative("assetPath").stringValue;
if (!string.IsNullOrWhiteSpace(entryName))
{
return entryName;
}
return string.IsNullOrWhiteSpace(assetPath) ? "<Unnamed Entry>" : assetPath;
}
private string GetSecondaryLabel(SerializedProperty property)
{
if (property == null)
{
return string.Empty;
}
string group = property.FindPropertyRelative("group").stringValue;
string assetPath = property.FindPropertyRelative("assetPath").stringValue;
return string.IsNullOrWhiteSpace(assetPath) ? group : $"{group} | {assetPath}";
}
private void ClampSelection()
{
if (_entryIndices.Count == 0)
{
_selectedIndex = 0;
return;
}
_selectedIndex = Mathf.Clamp(_selectedIndex, 0, _entryIndices.Count - 1);
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 3c4eefe489788e84ba9ec7a3c27daad2
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -1,6 +1,4 @@
using System.Collections.Generic;
using UnityEditor;
using UnityEditorInternal;
using UnityEngine;
namespace AlicizaX
@ -8,173 +6,12 @@ namespace AlicizaX
[CustomEditor(typeof(PoolConfigScriptableObject))]
public sealed class PoolConfigScriptableObjectEditor : UnityEditor.Editor
{
private const float VerticalSpacing = 4f;
private ReorderableList _configList;
private SerializedProperty _configsProperty;
private void OnEnable()
{
_configsProperty = serializedObject.FindProperty("configs");
_configList = new ReorderableList(serializedObject, _configsProperty, true, true, true, true)
{
drawHeaderCallback = rect => EditorGUI.LabelField(rect, "Pool Configs"),
drawElementCallback = DrawElement,
elementHeightCallback = GetElementHeight
};
}
public override void OnInspectorGUI()
{
serializedObject.Update();
EditorGUILayout.HelpBox(
"每条配置定义一条匹配规则;真正的池按具体 assetPath 实例化,不再共享一个目录级总容量。",
MessageType.Info);
EditorGUILayout.BeginHorizontal();
if (GUILayout.Button("Normalize"))
if (GUILayout.Button("Open Editor", GUILayout.Width(160f), GUILayout.Height(28f)))
{
serializedObject.ApplyModifiedProperties();
NormalizeAndSort(shouldSort: false);
serializedObject.Update();
PoolConfigEditorWindow.Open((PoolConfigScriptableObject)target);
}
if (GUILayout.Button("Normalize And Sort"))
{
serializedObject.ApplyModifiedProperties();
NormalizeAndSort(shouldSort: true);
serializedObject.Update();
}
EditorGUILayout.EndHorizontal();
EditorGUILayout.Space();
_configList.DoLayoutList();
serializedObject.ApplyModifiedProperties();
DrawValidation();
}
private void DrawElement(Rect rect, int index, bool isActive, bool isFocused)
{
SerializedProperty element = _configsProperty.GetArrayElementAtIndex(index);
rect.y += 2f;
float lineHeight = EditorGUIUtility.singleLineHeight;
float wideFieldWidth = rect.width * 0.6f;
float narrowFieldWidth = rect.width - wideFieldWidth - 6f;
SerializedProperty group = element.FindPropertyRelative("group");
SerializedProperty assetPath = element.FindPropertyRelative("assetPath");
SerializedProperty matchMode = element.FindPropertyRelative("matchMode");
SerializedProperty loaderType = element.FindPropertyRelative("resourceLoaderType");
SerializedProperty capacity = element.FindPropertyRelative("capacity");
SerializedProperty prewarmCount = element.FindPropertyRelative("prewarmCount");
SerializedProperty idleTimeout = element.FindPropertyRelative("instanceIdleTimeout");
SerializedProperty unloadDelay = element.FindPropertyRelative("prefabUnloadDelay");
SerializedProperty preloadOnInitialize = element.FindPropertyRelative("preloadOnInitialize");
Rect line1 = new Rect(rect.x, rect.y, rect.width, lineHeight);
Rect line2 = new Rect(rect.x, line1.yMax + VerticalSpacing, rect.width, lineHeight);
Rect line3Left = new Rect(rect.x, line2.yMax + VerticalSpacing, wideFieldWidth, lineHeight);
Rect line3Right = new Rect(line3Left.xMax + 6f, line3Left.y, narrowFieldWidth, lineHeight);
Rect line4Left = new Rect(rect.x, line3Left.yMax + VerticalSpacing, wideFieldWidth, lineHeight);
Rect line4Right = new Rect(line4Left.xMax + 6f, line4Left.y, narrowFieldWidth, lineHeight);
Rect line5Left = new Rect(rect.x, line4Left.yMax + VerticalSpacing, wideFieldWidth, lineHeight);
Rect line5Right = new Rect(line5Left.xMax + 6f, line5Left.y, narrowFieldWidth, lineHeight);
EditorGUI.PropertyField(line1, assetPath);
EditorGUI.PropertyField(line2, group);
EditorGUI.PropertyField(line3Left, matchMode);
EditorGUI.PropertyField(line3Right, loaderType);
EditorGUI.PropertyField(line4Left, capacity);
EditorGUI.PropertyField(line4Right, prewarmCount);
EditorGUI.PropertyField(line5Left, idleTimeout);
EditorGUI.PropertyField(line5Right, unloadDelay);
Rect line6 = new Rect(rect.x, line5Left.yMax + VerticalSpacing, rect.width, lineHeight);
EditorGUI.PropertyField(line6, preloadOnInitialize);
}
private float GetElementHeight(int index)
{
float lineHeight = EditorGUIUtility.singleLineHeight;
return lineHeight * 6f + VerticalSpacing * 7f;
}
private void NormalizeAndSort(bool shouldSort)
{
var asset = (PoolConfigScriptableObject)target;
asset.Normalize();
if (shouldSort)
{
asset.configs.Sort(PoolConfig.CompareByPriority);
}
EditorUtility.SetDirty(asset);
}
private void DrawValidation()
{
var asset = (PoolConfigScriptableObject)target;
if (asset.configs == null || asset.configs.Count == 0)
{
return;
}
List<string> warnings = BuildWarnings(asset.configs);
for (int i = 0; i < warnings.Count; i++)
{
EditorGUILayout.HelpBox(warnings[i], MessageType.Warning);
}
}
private static List<string> BuildWarnings(List<PoolConfig> configs)
{
var warnings = new List<string>();
for (int i = 0; i < configs.Count; i++)
{
PoolConfig config = configs[i];
if (config == null)
{
warnings.Add($"Element {i} is null.");
continue;
}
if (string.IsNullOrWhiteSpace(config.assetPath))
{
warnings.Add($"Element {i} has an empty asset path.");
}
if (config.matchMode == PoolMatchMode.Prefix && config.preloadOnInitialize)
{
warnings.Add($"Element {i} uses Prefix matching and preloadOnInitialize. Prefix rules cannot infer a concrete asset to prewarm.");
}
for (int j = i + 1; j < configs.Count; j++)
{
PoolConfig other = configs[j];
if (other == null)
{
continue;
}
bool duplicate =
string.Equals(config.group, other.group, System.StringComparison.Ordinal) &&
config.matchMode == other.matchMode &&
string.Equals(config.assetPath, other.assetPath, System.StringComparison.Ordinal);
if (duplicate)
{
warnings.Add($"Duplicate rule detected between elements {i} and {j}: {config.group}:{config.assetPath}.");
}
}
}
return warnings;
}
}
}

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: f4ebb80ad5f287f42b3b4993abafba1b
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,308 @@
using System;
using System.Collections.Generic;
using UnityEngine;
namespace AlicizaX
{
[DisallowMultipleComponent]
public sealed class PoolAutoRecycleAfterSeconds : MonoBehaviour, IPoolAutoRecycle
{
[Min(0f)]
public float delaySeconds = 1f;
public bool TryGetAutoRecycleDelay(out float delay)
{
delay = delaySeconds;
return delay > 0f;
}
}
[DisallowMultipleComponent]
public sealed class PoolAutoRecycleParticleStop : MonoBehaviour, IGameObjectPoolable
{
[SerializeField] private bool includeChildren = true;
private ParticleSystem[] _particleSystems;
private bool _subscribed;
public void OnPoolCreate()
{
_particleSystems = includeChildren
? GetComponentsInChildren<ParticleSystem>(true)
: GetComponents<ParticleSystem>();
}
public void OnPoolGet(in PoolSpawnContext context)
{
if (_particleSystems == null)
{
OnPoolCreate();
}
for (int i = 0; i < _particleSystems.Length; i++)
{
ParticleSystem particle = _particleSystems[i];
if (particle == null)
{
continue;
}
var main = particle.main;
main.stopAction = ParticleSystemStopAction.Callback;
particle.Clear(true);
particle.Play(true);
}
_subscribed = true;
}
public void OnPoolRelease()
{
_subscribed = false;
}
public void OnPoolDestroy()
{
_subscribed = false;
}
private void OnParticleSystemStopped()
{
if (!_subscribed || !AppServices.TryGet(out GameObjectPoolManager poolManager))
{
return;
}
poolManager.Release(gameObject);
}
}
[DisallowMultipleComponent]
public sealed class PoolResetRigidbodyState : MonoBehaviour, IPoolResettablePhysics
{
[SerializeField] private bool includeChildren = true;
private Rigidbody[] _rigidbodies3D;
private Rigidbody2D[] _rigidbodies2D;
private void EnsureCache()
{
if (_rigidbodies3D == null)
{
_rigidbodies3D = includeChildren ? GetComponentsInChildren<Rigidbody>(true) : GetComponents<Rigidbody>();
}
if (_rigidbodies2D == null)
{
_rigidbodies2D = includeChildren ? GetComponentsInChildren<Rigidbody2D>(true) : GetComponents<Rigidbody2D>();
}
}
public void ResetPhysicsState()
{
EnsureCache();
for (int i = 0; i < _rigidbodies3D.Length; i++)
{
Rigidbody body = _rigidbodies3D[i];
if (body == null)
{
continue;
}
body.velocity = Vector3.zero;
body.angularVelocity = Vector3.zero;
body.Sleep();
}
for (int i = 0; i < _rigidbodies2D.Length; i++)
{
Rigidbody2D body = _rigidbodies2D[i];
if (body == null)
{
continue;
}
body.velocity = Vector2.zero;
body.angularVelocity = 0f;
body.Sleep();
}
}
}
[DisallowMultipleComponent]
public sealed class PoolResetParticleSystems : MonoBehaviour, IPoolResettableVisual
{
[SerializeField] private bool includeChildren = true;
private ParticleSystem[] _particleSystems;
private void EnsureCache()
{
if (_particleSystems == null)
{
_particleSystems = includeChildren
? GetComponentsInChildren<ParticleSystem>(true)
: GetComponents<ParticleSystem>();
}
}
public void ResetVisualState()
{
EnsureCache();
for (int i = 0; i < _particleSystems.Length; i++)
{
ParticleSystem particle = _particleSystems[i];
if (particle == null)
{
continue;
}
particle.Stop(true, ParticleSystemStopBehavior.StopEmittingAndClear);
particle.Clear(true);
}
}
}
[DisallowMultipleComponent]
public sealed class PoolResetTrailRenderers : MonoBehaviour, IPoolResettableVisual
{
[SerializeField] private bool includeChildren = true;
private TrailRenderer[] _trailRenderers;
private void EnsureCache()
{
if (_trailRenderers == null)
{
_trailRenderers = includeChildren
? GetComponentsInChildren<TrailRenderer>(true)
: GetComponents<TrailRenderer>();
}
}
public void ResetVisualState()
{
EnsureCache();
for (int i = 0; i < _trailRenderers.Length; i++)
{
TrailRenderer trail = _trailRenderers[i];
if (trail == null)
{
continue;
}
trail.Clear();
}
}
}
[DisallowMultipleComponent]
public sealed class PoolResetAnimatorState : MonoBehaviour, IPoolResettableAnimation
{
[SerializeField] private bool includeChildren = true;
private Animator[] _animators;
private void EnsureCache()
{
if (_animators == null)
{
_animators = includeChildren ? GetComponentsInChildren<Animator>(true) : GetComponents<Animator>();
}
}
public void ResetAnimationState()
{
EnsureCache();
for (int i = 0; i < _animators.Length; i++)
{
Animator animator = _animators[i];
if (animator == null || !animator.isActiveAndEnabled)
{
continue;
}
animator.Rebind();
animator.Update(0f);
}
}
}
[DisallowMultipleComponent]
public sealed class PoolSleepableGameObjectGroup : MonoBehaviour, IPoolSleepable
{
[SerializeField] private List<Behaviour> disableOnSleep = new List<Behaviour>();
[SerializeField] private List<Renderer> hideOnSleep = new List<Renderer>();
[SerializeField] private List<Collider> disableCollider3D = new List<Collider>();
[SerializeField] private List<Collider2D> disableCollider2D = new List<Collider2D>();
public void EnterSleep()
{
for (int i = 0; i < disableOnSleep.Count; i++)
{
if (disableOnSleep[i] != null)
{
disableOnSleep[i].enabled = false;
}
}
for (int i = 0; i < hideOnSleep.Count; i++)
{
if (hideOnSleep[i] != null)
{
hideOnSleep[i].enabled = false;
}
}
for (int i = 0; i < disableCollider3D.Count; i++)
{
if (disableCollider3D[i] != null)
{
disableCollider3D[i].enabled = false;
}
}
for (int i = 0; i < disableCollider2D.Count; i++)
{
if (disableCollider2D[i] != null)
{
disableCollider2D[i].enabled = false;
}
}
}
public void ExitSleep(in PoolSpawnContext context)
{
for (int i = 0; i < disableOnSleep.Count; i++)
{
if (disableOnSleep[i] != null)
{
disableOnSleep[i].enabled = true;
}
}
for (int i = 0; i < hideOnSleep.Count; i++)
{
if (hideOnSleep[i] != null)
{
hideOnSleep[i].enabled = true;
}
}
for (int i = 0; i < disableCollider3D.Count; i++)
{
if (disableCollider3D[i] != null)
{
disableCollider3D[i].enabled = true;
}
}
for (int i = 0; i < disableCollider2D.Count; i++)
{
if (disableCollider2D[i] != null)
{
disableCollider2D[i].enabled = true;
}
}
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: fb76659ea7bc2ee4cba827951180c766
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -1,6 +1,6 @@
using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Serialization;
namespace AlicizaX
{
@ -16,49 +16,101 @@ namespace AlicizaX
Prefix = 1
}
/// <summary>
/// 对象池配置项。
/// </summary>
public enum PoolOverflowPolicy
{
FailFast = 0,
InstantiateOneShot = 1,
AutoExpand = 2,
RecycleOldestInactive = 3,
DropNewestRequest = 4
}
public enum PoolTrimPolicy
{
None = 0,
IdleOnly = 1,
IdleAndPriority = 2,
AggressiveOnLowMemory = 3
}
public enum PoolActivationMode
{
SetActive = 0,
SleepWake = 1,
Custom = 2
}
public enum PoolResetMode
{
TransformOnly = 0,
PoolableCallbacks = 1,
FullReset = 2,
Custom = 3
}
[Serializable]
public sealed class PoolConfig
public sealed class PoolEntry
{
public const string DefaultGroup = "Default";
public const int DefaultCapacity = 8;
public const float DefaultInstanceIdleTimeout = 30f;
public const float DefaultPrefabUnloadDelay = 60f;
public const string DefaultEntryName = "DefaultPool";
public string entryName = DefaultEntryName;
public string group = DefaultGroup;
[FormerlySerializedAs("asset")]
public string assetPath;
public PoolMatchMode matchMode = PoolMatchMode.Exact;
public PoolResourceLoaderType resourceLoaderType = PoolResourceLoaderType.AssetBundle;
[FormerlySerializedAs("time")]
[Min(0f)]
public float instanceIdleTimeout = DefaultInstanceIdleTimeout;
[Min(0f)]
public float prefabUnloadDelay = DefaultPrefabUnloadDelay;
[FormerlySerializedAs("poolCount")]
[Min(1)]
public int capacity = DefaultCapacity;
public PoolResourceLoaderType loaderType = PoolResourceLoaderType.AssetBundle;
public PoolOverflowPolicy overflowPolicy = PoolOverflowPolicy.FailFast;
public PoolTrimPolicy trimPolicy = PoolTrimPolicy.IdleOnly;
public PoolActivationMode activationMode = PoolActivationMode.SetActive;
public PoolResetMode resetMode = PoolResetMode.PoolableCallbacks;
[Min(0)]
public int prewarmCount;
public int minRetained = 0;
public bool preloadOnInitialize;
[Min(1)]
public int softCapacity = 8;
[Min(1)]
public int hardCapacity = 8;
[Min(0f)]
public float idleTrimDelay = 30f;
[Min(0f)]
public float prefabUnloadDelay = 60f;
[Min(0f)]
public float autoRecycleDelay = 0f;
[Min(1)]
public int trimBatchPerTick = 2;
[Min(1)]
public int warmupBatchPerFrame = 2;
[Min(0f)]
public float warmupFrameBudgetMs = 1f;
public bool allowRuntimeExpand;
public bool keepPrefabResident;
public bool aggressiveTrimOnLowMemory;
public int priority;
public void Normalize()
{
entryName = string.IsNullOrWhiteSpace(entryName) ? DefaultEntryName : entryName.Trim();
group = string.IsNullOrWhiteSpace(group) ? DefaultGroup : group.Trim();
assetPath = NormalizeAssetPath(assetPath);
capacity = Mathf.Max(1, capacity);
prewarmCount = Mathf.Clamp(prewarmCount, 0, capacity);
instanceIdleTimeout = Mathf.Max(0f, instanceIdleTimeout);
minRetained = Mathf.Max(0, minRetained);
softCapacity = Mathf.Max(1, softCapacity);
hardCapacity = Mathf.Max(softCapacity, hardCapacity);
minRetained = Mathf.Min(minRetained, hardCapacity);
idleTrimDelay = Mathf.Max(0f, idleTrimDelay);
prefabUnloadDelay = Mathf.Max(0f, prefabUnloadDelay);
autoRecycleDelay = Mathf.Max(0f, autoRecycleDelay);
trimBatchPerTick = Mathf.Max(1, trimBatchPerTick);
warmupBatchPerFrame = Mathf.Max(1, warmupBatchPerFrame);
warmupFrameBudgetMs = Mathf.Max(0f, warmupFrameBudgetMs);
}
public bool Matches(string requestedAssetPath, string requestedGroup = null)
@ -84,10 +136,11 @@ namespace AlicizaX
public string BuildResolvedPoolKey(string resolvedAssetPath)
{
return $"{group}|{(int)matchMode}|{assetPath}|{(int)resourceLoaderType}|{resolvedAssetPath}";
string concretePath = NormalizeAssetPath(resolvedAssetPath);
return $"{group}|{(int)matchMode}|{assetPath}|{(int)loaderType}|{concretePath}|{entryName}";
}
public static int CompareByPriority(PoolConfig left, PoolConfig right)
public static int CompareByPriority(PoolEntry left, PoolEntry right)
{
if (ReferenceEquals(left, right))
{
@ -104,13 +157,21 @@ namespace AlicizaX
return -1;
}
int priorityCompare = right.priority.CompareTo(left.priority);
if (priorityCompare != 0)
{
return priorityCompare;
}
int modeCompare = left.matchMode.CompareTo(right.matchMode);
if (modeCompare != 0)
{
return modeCompare;
}
int pathLengthCompare = right.assetPath.Length.CompareTo(left.assetPath.Length);
int leftLength = left.assetPath?.Length ?? 0;
int rightLength = right.assetPath?.Length ?? 0;
int pathLengthCompare = rightLength.CompareTo(leftLength);
if (pathLengthCompare != 0)
{
return pathLengthCompare;
@ -130,4 +191,81 @@ namespace AlicizaX
}
}
[Serializable]
public sealed class ResolvedPoolConfig
{
public string entryName;
public string group;
public string assetPath;
public PoolMatchMode matchMode;
public PoolResourceLoaderType loaderType;
public PoolOverflowPolicy overflowPolicy;
public PoolTrimPolicy trimPolicy;
public PoolActivationMode activationMode;
public PoolResetMode resetMode;
public int minRetained;
public int softCapacity;
public int hardCapacity;
public float idleTrimDelay;
public float prefabUnloadDelay;
public float autoRecycleDelay;
public int trimBatchPerTick;
public int warmupBatchPerFrame;
public float warmupFrameBudgetMs;
public bool allowRuntimeExpand;
public bool keepPrefabResident;
public bool aggressiveTrimOnLowMemory;
public int priority;
public string BuildResolvedPoolKey(string resolvedAssetPath)
{
string concretePath = PoolEntry.NormalizeAssetPath(resolvedAssetPath);
return $"{group}|{(int)matchMode}|{assetPath}|{(int)loaderType}|{concretePath}|{entryName}";
}
public static ResolvedPoolConfig From(PoolEntry entry)
{
if (entry == null)
{
throw new ArgumentNullException(nameof(entry));
}
return new ResolvedPoolConfig
{
entryName = entry.entryName,
group = entry.group,
assetPath = entry.assetPath,
matchMode = entry.matchMode,
loaderType = entry.loaderType,
overflowPolicy = entry.overflowPolicy,
trimPolicy = entry.trimPolicy,
activationMode = entry.activationMode,
resetMode = entry.resetMode,
minRetained = entry.minRetained,
softCapacity = entry.softCapacity,
hardCapacity = entry.hardCapacity,
idleTrimDelay = entry.idleTrimDelay,
prefabUnloadDelay = entry.prefabUnloadDelay,
autoRecycleDelay = entry.autoRecycleDelay,
trimBatchPerTick = entry.trimBatchPerTick,
warmupBatchPerFrame = entry.warmupBatchPerFrame,
warmupFrameBudgetMs = entry.warmupFrameBudgetMs,
allowRuntimeExpand = entry.allowRuntimeExpand,
keepPrefabResident = entry.keepPrefabResident,
aggressiveTrimOnLowMemory = entry.aggressiveTrimOnLowMemory,
priority = entry.priority
};
}
}
[Serializable]
public sealed class PoolConfigCatalog
{
public readonly List<PoolEntry> entries;
public PoolConfigCatalog(List<PoolEntry> entries)
{
this.entries = entries ?? throw new ArgumentNullException(nameof(entries));
}
}
}

View File

@ -1,3 +1,11 @@
fileFormatVersion: 2
guid: 7de4b0d73b4145b6bd836ec9d2c832d9
timeCreated: 1774439841
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -6,19 +6,38 @@ namespace AlicizaX
[CreateAssetMenu(fileName = "PoolConfig", menuName = "GameplaySystem/PoolConfig", order = 10)]
public class PoolConfigScriptableObject : ScriptableObject
{
public List<PoolConfig> configs = new List<PoolConfig>();
public List<PoolEntry> entries = new List<PoolEntry>();
public PoolConfigCatalog BuildCatalog()
{
Normalize();
var normalizedEntries = new List<PoolEntry>(entries.Count);
for (int i = 0; i < entries.Count; i++)
{
PoolEntry entry = entries[i];
if (entry == null)
{
continue;
}
normalizedEntries.Add(entry);
}
normalizedEntries.Sort(PoolEntry.CompareByPriority);
return new PoolConfigCatalog(normalizedEntries);
}
public void Normalize()
{
if (configs == null)
if (entries == null)
{
configs = new List<PoolConfig>();
return;
entries = new List<PoolEntry>();
}
for (int i = 0; i < configs.Count; i++)
for (int i = 0; i < entries.Count; i++)
{
configs[i]?.Normalize();
entries[i]?.Normalize();
}
}
@ -29,5 +48,4 @@ namespace AlicizaX
}
#endif
}
}

View File

@ -18,22 +18,18 @@ namespace AlicizaX
return groupCompare != 0 ? groupCompare : string.Compare(left.assetPath, right.assetPath, StringComparison.Ordinal);
};
[Header("检查间隔")] public float checkInterval = 10f;
[Header("配置路径")] public string poolConfigPath = "Assets/Bundles/Configs/PoolConfig";
[Header("Inspector显示设置")] public bool showDetailedInfo = true;
[Header("Cleanup Interval")] public float checkInterval = 5f;
[Header("Config Path")] public string poolConfigPath = "Assets/Bundles/Configs/PoolConfig";
[Header("Show Detailed Info")] public bool showDetailedInfo = true;
[SerializeField] internal Transform poolContainer;
private const PoolResourceLoaderType DefaultDirectLoadResourceLoaderType = PoolResourceLoaderType.AssetBundle;
private readonly Dictionary<string, RuntimePrefabPool> _poolsByKey = new Dictionary<string, RuntimePrefabPool>(StringComparer.Ordinal);
private readonly Dictionary<string, PoolConfig> _resolvedConfigCache = new Dictionary<string, PoolConfig>(StringComparer.Ordinal);
private readonly Dictionary<string, PoolResourceLoaderType> _groupLoaderCache = new Dictionary<string, PoolResourceLoaderType>(StringComparer.Ordinal);
private readonly Dictionary<GameObject, RuntimePrefabPool> _ownersByObject = new Dictionary<GameObject, RuntimePrefabPool>();
private readonly Dictionary<string, ResolvedPoolConfig> _resolvedConfigCache = new Dictionary<string, ResolvedPoolConfig>(StringComparer.Ordinal);
private readonly Dictionary<string, RuntimePrefabPool> _ownersByObject = new Dictionary<string, RuntimePrefabPool>(StringComparer.Ordinal);
private readonly Dictionary<PoolResourceLoaderType, IResourceLoader> _resourceLoaders = new Dictionary<PoolResourceLoaderType, IResourceLoader>();
private readonly List<PoolConfig> _configs = new List<PoolConfig>();
private readonly List<PoolEntry> _entries = new List<PoolEntry>();
private readonly List<GameObjectPoolSnapshot> _debugSnapshots = new List<GameObjectPoolSnapshot>();
private CancellationTokenSource _shutdownTokenSource;
@ -42,8 +38,8 @@ namespace AlicizaX
private Exception _initializationException;
private bool _cleanupScheduled;
private float _nextCleanupTime = float.MaxValue;
private bool _isShuttingDown;
private bool _aggressiveCleanupRequested;
public bool IsReady => _initializationCompleted && _initializationException == null;
@ -80,7 +76,6 @@ namespace AlicizaX
await UniTask.WaitUntil(() => YooAsset.YooAssets.Initialized, cancellationToken: cancellationToken);
LoadConfigs();
_initializationCompleted = true;
await PrewarmConfiguredPoolsAsync(cancellationToken);
ScheduleCleanup();
}
catch (OperationCanceledException)
@ -107,7 +102,8 @@ namespace AlicizaX
return;
}
PerformCleanup();
PerformCleanup(_aggressiveCleanupRequested);
_aggressiveCleanupRequested = false;
ScheduleCleanup();
}
@ -126,35 +122,161 @@ namespace AlicizaX
_resourceLoaders[loaderType] = resourceLoader;
}
public GameObject GetGameObject(string assetPath, Transform parent = null)
public GameObject GetGameObject(string assetPath, Transform parent = null, object userData = null)
{
EnsureReadyForSyncUse();
return GetGameObjectInternal(PoolConfig.NormalizeAssetPath(assetPath), null, parent);
string normalized = PoolEntry.NormalizeAssetPath(assetPath);
PoolSpawnContext context = PoolSpawnContext.Create(normalized, null, parent, userData);
return GetGameObjectInternal(normalized, null, context);
}
public GameObject GetGameObjectByGroup(string group, string assetPath, Transform parent = null)
public GameObject GetGameObject(string assetPath, in PoolSpawnContext context)
{
EnsureReadyForSyncUse();
return GetGameObjectInternal(PoolConfig.NormalizeAssetPath(assetPath), group, parent);
string normalized = PoolEntry.NormalizeAssetPath(assetPath);
PoolSpawnContext resolvedContext = new PoolSpawnContext(
normalized,
context.Group,
context.Parent,
context.Position,
context.Rotation,
context.UserData,
context.OwnerId,
context.TeamId,
context.SpawnFrame == 0 ? (uint)Time.frameCount : context.SpawnFrame);
return GetGameObjectInternal(normalized, context.Group, resolvedContext);
}
public GameObject GetGameObject(
string assetPath,
Vector3 position,
Quaternion rotation,
Transform parent = null,
object userData = null)
{
EnsureReadyForSyncUse();
string normalized = PoolEntry.NormalizeAssetPath(assetPath);
PoolSpawnContext context = new PoolSpawnContext(
normalized,
null,
parent,
position,
rotation,
userData,
spawnFrame: (uint)Time.frameCount);
return GetGameObjectInternal(normalized, null, context);
}
public GameObject GetGameObjectByGroup(string group, string assetPath, Transform parent = null, object userData = null)
{
EnsureReadyForSyncUse();
string normalized = PoolEntry.NormalizeAssetPath(assetPath);
PoolSpawnContext context = PoolSpawnContext.Create(normalized, group, parent, userData);
return GetGameObjectInternal(normalized, group, context);
}
public GameObject GetGameObjectByGroup(string group, string assetPath, in PoolSpawnContext context)
{
EnsureReadyForSyncUse();
string normalized = PoolEntry.NormalizeAssetPath(assetPath);
PoolSpawnContext resolvedContext = new PoolSpawnContext(
normalized,
group,
context.Parent,
context.Position,
context.Rotation,
context.UserData,
context.OwnerId,
context.TeamId,
context.SpawnFrame == 0 ? (uint)Time.frameCount : context.SpawnFrame);
return GetGameObjectInternal(normalized, group, resolvedContext);
}
public GameObject GetGameObjectByGroup(
string group,
string assetPath,
Vector3 position,
Quaternion rotation,
Transform parent = null,
object userData = null)
{
EnsureReadyForSyncUse();
string normalized = PoolEntry.NormalizeAssetPath(assetPath);
PoolSpawnContext context = new PoolSpawnContext(
normalized,
group,
parent,
position,
rotation,
userData,
spawnFrame: (uint)Time.frameCount);
return GetGameObjectInternal(normalized, group, context);
}
public async UniTask<GameObject> GetGameObjectAsync(
string assetPath,
Transform parent = null,
object userData = null,
CancellationToken cancellationToken = default)
{
await EnsureInitializedAsync(cancellationToken);
return await GetGameObjectInternalAsync(PoolConfig.NormalizeAssetPath(assetPath), null, parent, cancellationToken);
string normalized = PoolEntry.NormalizeAssetPath(assetPath);
PoolSpawnContext context = PoolSpawnContext.Create(normalized, null, parent, userData);
return await GetGameObjectInternalAsync(normalized, null, context, cancellationToken);
}
public async UniTask<GameObject> GetGameObjectAsync(
string assetPath,
PoolSpawnContext context,
CancellationToken cancellationToken = default)
{
await EnsureInitializedAsync(cancellationToken);
string normalized = PoolEntry.NormalizeAssetPath(assetPath);
PoolSpawnContext resolvedContext = new PoolSpawnContext(
normalized,
context.Group,
context.Parent,
context.Position,
context.Rotation,
context.UserData,
context.OwnerId,
context.TeamId,
context.SpawnFrame == 0 ? (uint)Time.frameCount : context.SpawnFrame);
return await GetGameObjectInternalAsync(normalized, context.Group, resolvedContext, cancellationToken);
}
public async UniTask<GameObject> GetGameObjectAsyncByGroup(
string group,
string assetPath,
Transform parent = null,
object userData = null,
CancellationToken cancellationToken = default)
{
await EnsureInitializedAsync(cancellationToken);
return await GetGameObjectInternalAsync(PoolConfig.NormalizeAssetPath(assetPath), group, parent, cancellationToken);
string normalized = PoolEntry.NormalizeAssetPath(assetPath);
PoolSpawnContext context = PoolSpawnContext.Create(normalized, group, parent, userData);
return await GetGameObjectInternalAsync(normalized, group, context, cancellationToken);
}
public async UniTask<GameObject> GetGameObjectAsyncByGroup(
string group,
string assetPath,
PoolSpawnContext context,
CancellationToken cancellationToken = default)
{
await EnsureInitializedAsync(cancellationToken);
string normalized = PoolEntry.NormalizeAssetPath(assetPath);
PoolSpawnContext resolvedContext = new PoolSpawnContext(
normalized,
group,
context.Parent,
context.Position,
context.Rotation,
context.UserData,
context.OwnerId,
context.TeamId,
context.SpawnFrame == 0 ? (uint)Time.frameCount : context.SpawnFrame);
return await GetGameObjectInternalAsync(normalized, group, resolvedContext, cancellationToken);
}
public void Release(GameObject gameObject)
@ -164,7 +286,8 @@ namespace AlicizaX
return;
}
if (_ownersByObject.TryGetValue(gameObject, out RuntimePrefabPool pool))
string instanceKey = GetInstanceKey(gameObject);
if (_ownersByObject.TryGetValue(instanceKey, out RuntimePrefabPool pool))
{
pool.Release(gameObject);
return;
@ -174,22 +297,10 @@ namespace AlicizaX
Destroy(gameObject);
}
public void Preload(string assetPath, int count = 1)
{
EnsureReadyForSyncUse();
PreloadInternal(PoolConfig.NormalizeAssetPath(assetPath), null, count);
}
public void PreloadByGroup(string group, string assetPath, int count = 1)
{
EnsureReadyForSyncUse();
PreloadInternal(PoolConfig.NormalizeAssetPath(assetPath), group, count);
}
public async UniTask PreloadAsync(string assetPath, int count = 1, CancellationToken cancellationToken = default)
{
await EnsureInitializedAsync(cancellationToken);
await PreloadInternalAsync(PoolConfig.NormalizeAssetPath(assetPath), null, count, cancellationToken);
await PreloadInternalAsync(PoolEntry.NormalizeAssetPath(assetPath), null, count, cancellationToken);
}
public async UniTask PreloadAsyncByGroup(
@ -199,7 +310,7 @@ namespace AlicizaX
CancellationToken cancellationToken = default)
{
await EnsureInitializedAsync(cancellationToken);
await PreloadInternalAsync(PoolConfig.NormalizeAssetPath(assetPath), group, count, cancellationToken);
await PreloadInternalAsync(PoolEntry.NormalizeAssetPath(assetPath), group, count, cancellationToken);
}
public void ForceCleanup()
@ -209,7 +320,7 @@ namespace AlicizaX
return;
}
PerformCleanup();
PerformCleanup(false);
ScheduleCleanup();
}
@ -223,11 +334,10 @@ namespace AlicizaX
}
_isShuttingDown = false;
_poolsByKey.Clear();
_ownersByObject.Clear();
_resolvedConfigCache.Clear();
_groupLoaderCache.Clear();
_entries.Clear();
ReleaseDebugSnapshots();
_cleanupScheduled = false;
_nextCleanupTime = float.MaxValue;
@ -244,7 +354,6 @@ namespace AlicizaX
}
_debugSnapshots.Sort(SnapshotComparer);
return _debugSnapshots;
}
@ -255,7 +364,7 @@ namespace AlicizaX
return;
}
_ownersByObject[gameObject] = pool;
_ownersByObject[GetInstanceKey(gameObject)] = pool;
NotifyPoolStateChanged();
}
@ -266,7 +375,7 @@ namespace AlicizaX
return;
}
_ownersByObject.Remove(gameObject);
_ownersByObject.Remove(GetInstanceKey(gameObject));
NotifyPoolStateChanged();
}
@ -280,50 +389,32 @@ namespace AlicizaX
ScheduleCleanup();
}
private GameObject GetGameObjectInternal(string assetPath, string group, Transform parent)
private GameObject GetGameObjectInternal(string assetPath, string group, PoolSpawnContext context)
{
PoolConfig config = ResolveConfig(assetPath, group);
ResolvedPoolConfig config = ResolveConfig(assetPath, group);
if (config == null)
{
return LoadUnpooled(assetPath, group, parent);
return LoadUnpooled(assetPath, group, context.Parent);
}
RuntimePrefabPool pool = GetOrCreatePool(config, assetPath);
return pool.Acquire(parent);
return pool.Acquire(context);
}
private async UniTask<GameObject> GetGameObjectInternalAsync(
string assetPath,
string group,
Transform parent,
PoolSpawnContext context,
CancellationToken cancellationToken)
{
PoolConfig config = ResolveConfig(assetPath, group);
ResolvedPoolConfig config = ResolveConfig(assetPath, group);
if (config == null)
{
return await LoadUnpooledAsync(assetPath, group, parent, cancellationToken);
return await LoadUnpooledAsync(assetPath, group, context.Parent, cancellationToken);
}
RuntimePrefabPool pool = GetOrCreatePool(config, assetPath);
return await pool.AcquireAsync(parent, cancellationToken);
}
private void PreloadInternal(string assetPath, string group, int count)
{
if (count <= 0)
{
return;
}
PoolConfig config = ResolveConfig(assetPath, group);
if (config == null)
{
Log.Warning($"Asset '{assetPath}' has no matching pool config. Preload skipped.");
return;
}
RuntimePrefabPool pool = GetOrCreatePool(config, assetPath);
pool.Warmup(count);
return await pool.AcquireAsync(context, cancellationToken);
}
private async UniTask PreloadInternalAsync(
@ -337,10 +428,10 @@ namespace AlicizaX
return;
}
PoolConfig config = ResolveConfig(assetPath, group);
ResolvedPoolConfig config = ResolveConfig(assetPath, group);
if (config == null)
{
Log.Warning($"Asset '{assetPath}' has no matching pool config. Preload skipped.");
Log.Warning($"Asset '{assetPath}' has no matching pool rule. Preload skipped.");
return;
}
@ -348,7 +439,7 @@ namespace AlicizaX
await pool.WarmupAsync(count, cancellationToken);
}
private RuntimePrefabPool GetOrCreatePool(PoolConfig config, string assetPath)
private RuntimePrefabPool GetOrCreatePool(ResolvedPoolConfig config, string assetPath)
{
EnsurePoolContainer();
@ -362,7 +453,7 @@ namespace AlicizaX
pool.Initialize(
config,
assetPath,
GetResourceLoader(config.resourceLoaderType),
GetResourceLoader(config.loaderType),
this,
_shutdownTokenSource != null ? _shutdownTokenSource.Token : default);
@ -371,12 +462,11 @@ namespace AlicizaX
return pool;
}
private void PerformCleanup()
private void PerformCleanup(bool aggressive)
{
float now = Time.time;
foreach (RuntimePrefabPool pool in _poolsByKey.Values)
{
pool.TrimExpiredInstances(now);
pool.Trim(aggressive ? int.MaxValue : 0, aggressive);
}
}
@ -390,20 +480,21 @@ namespace AlicizaX
return;
}
float nextDueTime = float.MaxValue;
float now = Time.time;
bool hasWork = false;
float nextDueTime = now + Mathf.Max(checkInterval, 0.1f);
foreach (RuntimePrefabPool pool in _poolsByKey.Values)
{
float candidate = pool.GetNextCleanupTime(now, checkInterval);
if (candidate < nextDueTime)
if (pool.NeedsTrim(now) || pool.NeedsPrefabUnload(now, false))
{
nextDueTime = candidate;
hasWork = true;
break;
}
}
_nextCleanupTime = nextDueTime;
_cleanupScheduled = nextDueTime < float.MaxValue;
enabled = _cleanupScheduled;
_nextCleanupTime = hasWork ? nextDueTime : float.MaxValue;
_cleanupScheduled = hasWork;
enabled = hasWork;
}
private void OnLowMemory()
@ -413,7 +504,8 @@ namespace AlicizaX
return;
}
PerformCleanup();
_aggressiveCleanupRequested = true;
PerformCleanup(true);
ScheduleCleanup();
}
@ -444,117 +536,50 @@ namespace AlicizaX
private void LoadConfigs()
{
_configs.Clear();
_entries.Clear();
_resolvedConfigCache.Clear();
_groupLoaderCache.Clear();
IResourceService resourceService = AppServices.Require<IResourceService>();
PoolConfigScriptableObject configAsset = resourceService.LoadAsset<PoolConfigScriptableObject>(poolConfigPath);
if (configAsset == null || configAsset.configs == null)
if (configAsset == null)
{
return;
}
for (int i = 0; i < configAsset.configs.Count; i++)
{
PoolConfig config = configAsset.configs[i];
if (config == null)
{
continue;
}
config.Normalize();
if (string.IsNullOrWhiteSpace(config.assetPath))
{
Log.Warning($"PoolConfig at index {i} has an empty asset path and was ignored.");
continue;
}
_configs.Add(config);
}
_configs.Sort(PoolConfig.CompareByPriority);
LogConfigWarnings();
for (int i = 0; i < _configs.Count; i++)
{
PoolConfig config = _configs[i];
if (!_groupLoaderCache.ContainsKey(config.group))
{
_groupLoaderCache[config.group] = config.resourceLoaderType;
}
}
PoolConfigCatalog catalog = configAsset.BuildCatalog();
_entries.AddRange(catalog.entries);
resourceService.UnloadAsset(configAsset);
}
private async UniTask PrewarmConfiguredPoolsAsync(CancellationToken cancellationToken)
{
for (int i = 0; i < _configs.Count; i++)
{
PoolConfig config = _configs[i];
if (!config.preloadOnInitialize || config.prewarmCount <= 0)
{
continue;
}
if (config.matchMode != PoolMatchMode.Exact)
{
Log.Warning(
$"PoolConfig '{config.assetPath}' uses Prefix mode and preloadOnInitialize. Prefix rules cannot infer a concrete asset to prewarm.");
continue;
}
RuntimePrefabPool pool = GetOrCreatePool(config, config.assetPath);
await pool.WarmupAsync(config.prewarmCount, cancellationToken);
}
}
private PoolConfig ResolveConfig(string assetPath, string group)
private ResolvedPoolConfig ResolveConfig(string assetPath, string group)
{
if (string.IsNullOrWhiteSpace(assetPath))
{
return null;
}
bool hasGroup = !string.IsNullOrEmpty(group);
string cacheKey = hasGroup ? string.Concat(group, "|", assetPath) : assetPath;
if (_resolvedConfigCache.TryGetValue(cacheKey, out PoolConfig cachedConfig))
string cacheKey = string.IsNullOrWhiteSpace(group) ? assetPath : string.Concat(group, "|", assetPath);
if (_resolvedConfigCache.TryGetValue(cacheKey, out ResolvedPoolConfig cachedConfig))
{
return cachedConfig;
}
PoolConfig matchedConfig = null;
for (int i = 0; i < _configs.Count; i++)
for (int i = 0; i < _entries.Count; i++)
{
PoolConfig candidate = _configs[i];
if (!candidate.Matches(assetPath, group))
PoolEntry entry = _entries[i];
if (!entry.Matches(assetPath, group))
{
continue;
}
if (matchedConfig == null)
{
matchedConfig = candidate;
continue;
}
bool samePriority =
matchedConfig.matchMode == candidate.matchMode &&
matchedConfig.assetPath.Length == candidate.assetPath.Length;
if (samePriority)
{
Log.Warning(
$"Asset '{assetPath}' matched multiple pool configs with the same priority. Using '{matchedConfig.group}:{matchedConfig.assetPath}'.");
}
break;
ResolvedPoolConfig resolved = ResolvedPoolConfig.From(entry);
_resolvedConfigCache[cacheKey] = resolved;
return resolved;
}
_resolvedConfigCache[cacheKey] = matchedConfig;
return matchedConfig;
_resolvedConfigCache[cacheKey] = null;
return null;
}
private GameObject LoadUnpooled(string assetPath, string group, Transform parent)
@ -575,10 +600,16 @@ namespace AlicizaX
private IResourceLoader GetDirectLoadResourceLoader(string group)
{
if (!string.IsNullOrWhiteSpace(group) &&
_groupLoaderCache.TryGetValue(group, out PoolResourceLoaderType loaderType))
if (!string.IsNullOrWhiteSpace(group))
{
return GetResourceLoader(loaderType);
for (int i = 0; i < _entries.Count; i++)
{
PoolEntry entry = _entries[i];
if (string.Equals(entry.group, group, StringComparison.Ordinal))
{
return GetResourceLoader(entry.loaderType);
}
}
}
return GetResourceLoader(DefaultDirectLoadResourceLoaderType);
@ -618,20 +649,6 @@ namespace AlicizaX
}
}
private void LogConfigWarnings()
{
var seen = new HashSet<(string group, string assetPath)>();
for (int i = 0; i < _configs.Count; i++)
{
PoolConfig config = _configs[i];
var key = (config.group, config.assetPath);
if (!seen.Add(key))
{
Log.Warning($"Duplicate pool config detected: '{config.group}:{config.assetPath}'.");
}
}
}
private void ReleaseDebugSnapshots()
{
for (int i = 0; i < _debugSnapshots.Count; i++)
@ -641,5 +658,10 @@ namespace AlicizaX
_debugSnapshots.Clear();
}
private static string GetInstanceKey(GameObject gameObject)
{
return gameObject.GetInstanceID().ToString();
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,722 @@
# GameObjectPool 最优架构设计文档
## 1. 文档目标
本文档定义 `GameObjectPool` 模块的目标形态,用于支撑游戏内所有 `GameObject` 相关对象的统一池化,包括但不限于:
- 怪物 / NPC
- 子弹 / 投射物
- 特效 / 粒子 / VFXGraph
- 掉落物 / 临时交互物
- 飘字 / World UI
- 音效壳对象
- 可复用的场景装饰物
目标不是做一个“能复用 prefab 的基础对象池”,而是做一个“可覆盖全项目高频对象生命周期管理”的统一池化体系。
---
## 2. 现状结论
当前实现可以作为基础版对象池使用,但不是最优方案,主要原因如下:
- 只处理 `SetActive`、`SetParent`、Transform 复位,不具备统一的业务状态重置协议
- 池满时只会返回 `null`,没有按对象类型区分溢出策略
- 预热只支持静态固定值,不支持场景级、战斗级、波次级预热
- 淘汰策略只基于时间,不包含优先级、预算、低内存策略
- 清理调度依赖全量扫描,不适合大量池并存
- 配置维度不足,无法精细支撑怪物、特效、子弹等不同对象类型
- 缺少自动回收能力
- 缺少运行时统计闭环,无法逼近最优容量配置
结论:
- 当前实现可以继续作为底层雏形
- 不建议直接扩写成最终方案
- 最优方向应升级为“分层池化架构”
---
## 3. 设计目标
### 3.1 功能目标
- 统一管理所有 `GameObject` 池化对象
- 支持同步获取、异步获取、预热、回收、自动回收
- 支持对象在复用前后执行自定义重置
- 支持按对象类型定义不同的容量与淘汰策略
- 支持运行时统计与调优建议导出
- 支持低内存场景下的激进回收
### 3.2 性能目标
- 高频借还路径保持 O(1) 或近似 O(1)
- 避免重复加载同一 prefab
- 避免单帧大量 Instantiate / Destroy
- 清理与预热都采用预算驱动,而不是无上限循环
- 复杂对象允许使用轻量休眠,而非强制全量 `SetActive`
### 3.3 工程目标
- 配置清晰,可由策划 / TA / 程序共同维护
- 业务接入方式统一,不让每个系统各自发明对象池
- 支持逐步迁移,兼容现有 `GameObjectPoolManager`
---
## 4. 总体架构
推荐采用三层结构。
### 4.1 第一层Prefab 池层
职责:
- prefab 资源加载与引用持有
- 单 prefab 实例池管理
- 容量控制
- 预热
- 淘汰
- 调度
建议保留当前 `RuntimePrefabPool` 的职责方向,但增强其策略能力。
### 4.2 第二层:实例生命周期层
职责:
- 统一对象创建后初始化
- 对象借出前重置
- 对象归还前清理
- 对象销毁前释放内部资源
- 管理自动回收与上下文注入
该层是当前模块最缺失的部分,也是怪物、子弹、特效能够统一纳入池化的关键。
### 4.3 第三层:业务门面层
职责:
- 向业务提供强类型 API
- 隐藏字符串路径、组名、预热细节
- 承载特定对象的默认策略
示例:
```csharp
Enemy enemy = enemyPool.Spawn(enemyId, spawnContext);
Bullet bullet = bulletPool.Spawn(bulletConfigId, spawnContext);
FxHandle fx = fxPool.Play(fxId, position, rotation, autoRecycle: true);
```
不建议把字符串 `assetPath` 作为业务层唯一入口。
字符串路径池应作为底层资源定位方式,而不是最终业务接口。
---
## 5. 生命周期协议设计
统一池化必须提供明确的生命周期协议。
### 5.1 核心接口
```csharp
public interface IGameObjectPoolable
{
void OnPoolCreate();
void OnPoolGet(in PoolSpawnContext context);
void OnPoolRelease();
void OnPoolDestroy();
}
```
语义建议:
- `OnPoolCreate`
仅在实例第一次创建后调用一次
- `OnPoolGet`
每次借出时调用,用于注入上下文、重置运行态
- `OnPoolRelease`
每次归还时调用,用于停止逻辑、清理状态
- `OnPoolDestroy`
实例真正销毁前调用,用于释放自持资源
### 5.2 可选扩展接口
```csharp
public interface IPoolAutoRecycle
{
bool TryGetAutoRecycleDelay(out float delaySeconds);
}
public interface IPoolResettablePhysics
{
void ResetPhysicsState();
}
public interface IPoolResettableVisual
{
void ResetVisualState();
}
public interface IPoolResettableAnimation
{
void ResetAnimationState();
}
public interface IPoolSleepable
{
void EnterSleep();
void ExitSleep(in PoolSpawnContext context);
}
```
说明:
- `IPoolSleepable` 主要给怪物 / NPC 这类重对象使用
- `IPoolResettablePhysics` 适合子弹、投射物、掉落物
- `IPoolResettableVisual` 适合特效、Trail、VFXGraph、Renderer 状态清理
### 5.3 PoolSpawnContext
`OnPoolGet` 需要统一上下文,避免靠外部脚本到处手动赋值。
建议结构:
```csharp
public readonly struct PoolSpawnContext
{
public readonly string AssetPath;
public readonly string Group;
public readonly Transform Parent;
public readonly Vector3 Position;
public readonly Quaternion Rotation;
public readonly object UserData;
public readonly int OwnerId;
public readonly int TeamId;
public readonly uint SpawnFrame;
}
```
说明:
- `UserData` 用于挂通用扩展参数
- 高频场景可以进一步拆成强类型上下文
---
## 6. 池实例状态机
每个实例建议具备明确状态:
- `Uninitialized`
- `Inactive`
- `Active`
- `Releasing`
- `Destroying`
状态转换:
```text
Create -> Inactive -> Active -> Releasing -> Inactive
Create -> Inactive -> Destroying -> Destroy
Active -> Destroying -> Destroy
```
价值:
- 避免重复回收
- 避免回收中再次借出
- 便于统计与调试
- 便于做自动回收与异步保护
---
## 7. 配置模型设计
当前 `PoolConfig` 维度不够,建议升级为两层配置:
- `PoolProfile`
- `PoolRule`
### 7.1 PoolProfile
定义一类对象的默认策略,例如:
- `BulletProfile`
- `FxProfile`
- `EnemyProfile`
- `UiWorldProfile`
示例字段:
```csharp
public sealed class PoolProfile
{
public string profileName;
public PoolObjectKind objectKind;
public PoolOverflowPolicy overflowPolicy;
public PoolTrimPolicy trimPolicy;
public PoolActivationMode activationMode;
public PoolResetMode resetMode;
public int minRetained;
public int softCapacity;
public int hardCapacity;
public float idleTrimDelay;
public float prefabUnloadDelay;
public float autoRecycleDelay;
public int trimBatchPerTick;
public int warmupBatchPerFrame;
public float warmupFrameBudgetMs;
public bool allowRuntimeExpand;
public bool preloadOnInitialize;
public bool keepPrefabResident;
public bool aggressiveTrimOnLowMemory;
}
```
### 7.2 PoolRule
定义具体资源命中规则。
```csharp
public sealed class PoolRule
{
public string group;
public string assetPath;
public PoolMatchMode matchMode;
public PoolResourceLoaderType loaderType;
public string profileName;
public bool overrideCapacity;
public int minRetained;
public int softCapacity;
public int hardCapacity;
public bool overridePrewarm;
public int prewarmCount;
public PoolPrewarmPhase prewarmPhase;
public int priority;
}
```
设计原则:
- `Profile` 管类型策略
- `Rule` 管具体命中
- 高频热点对象优先使用 `Exact`
- `Prefix` 只做默认兜底
---
## 8. 核心枚举草案
### 8.1 对象类型
```csharp
public enum PoolObjectKind
{
Default = 0,
Bullet = 1,
Effect = 2,
Enemy = 3,
Npc = 4,
Pickup = 5,
WorldUi = 6,
AudioProxy = 7,
SceneProp = 8
}
```
### 8.2 池满策略
```csharp
public enum PoolOverflowPolicy
{
FailFast = 0,
InstantiateOneShot = 1,
AutoExpand = 2,
RecycleOldestInactive = 3,
DropNewestRequest = 4
}
```
### 8.3 清理策略
```csharp
public enum PoolTrimPolicy
{
None = 0,
IdleOnly = 1,
IdleAndPriority = 2,
AggressiveOnLowMemory = 3
}
```
### 8.4 激活模式
```csharp
public enum PoolActivationMode
{
SetActive = 0,
SleepWake = 1,
Custom = 2
}
```
### 8.5 重置模式
```csharp
public enum PoolResetMode
{
TransformOnly = 0,
PoolableCallbacks = 1,
FullReset = 2,
Custom = 3
}
```
### 8.6 预热阶段
```csharp
public enum PoolPrewarmPhase
{
None = 0,
AppInitialize = 1,
SceneLoad = 2,
BattlePrepare = 3,
WavePrepare = 4,
OnDemand = 5
}
```
---
## 9. 不同对象类型的推荐策略
### 9.1 子弹
建议:
- `softCapacity` 较高
- `hardCapacity` 可比 `softCapacity` 高 1.5 到 2 倍
- 默认支持 `AutoExpand``InstantiateOneShot`
- 必须支持自动回收
- 必须清理 `Rigidbody / Collider / TrailRenderer`
- prefab 通常常驻,不建议频繁 unload
### 9.2 特效 / 粒子
建议:
- 区分关键特效与普通特效
- 普通特效允许 `DropNewestRequest`
- 回收时必须清理所有粒子系统、Trail、音效播放状态
- 支持“播完自动回池”
- 预热由场景和技能装配共同驱动
### 9.3 怪物 / NPC
建议:
- 不建议只依赖通用 `SetActive`
- 必须实现 `IGameObjectPoolable`
- 推荐支持 `SleepWake`
- 必须有 `minRetained`
- 池满时不建议直接 `null`
- 场景切换 / 波次结束后再做结构性收缩
### 9.4 掉落物 / 临时交互物
建议:
- 适合通用池
- 支持超时自动回收
- 支持来源信息重置
### 9.5 飘字 / World UI
建议:
- 高度适合池化
- 必须自动回收
- 必须重置 tween / alpha / scale / text / follow target
---
## 10. 预热策略设计
### 10.1 预热阶段
预热必须是分阶段的,而不是只靠初始化时一次性完成。
建议阶段:
- `AppInitialize`
- `SceneLoad`
- `BattlePrepare`
- `WavePrepare`
- `OnDemand`
### 10.2 预热预算
预热不允许无限制同步创建。
建议加入:
- 每帧最大创建数 `warmupBatchPerFrame`
- 每帧时间预算 `warmupFrameBudgetMs`
- 大对象延迟到非关键帧创建
### 10.3 自适应预热
建议收集以下统计并用于反哺配置:
- `peakActive`
- `peakTotal`
- `missCount`
- `expandCount`
- `poolExhaustedCount`
由此生成:
- 推荐 `softCapacity`
- 推荐 `prewarmCount`
- 推荐 `minRetained`
---
## 11. 淘汰策略设计
### 11.1 默认原则
淘汰不能只看时间,还应结合:
- 空闲时间
- 对象优先级
- 当前是否战斗中
- 当前内存压力
- 当前池容量是否超过软上限
### 11.2 推荐容量模型
- `minRetained`
最低保有量,正常回收不低于该值
- `softCapacity`
常态推荐容量
- `hardCapacity`
绝对上限
### 11.3 销毁预算
回收策略必须预算化:
- 每 tick 最多回收 `trimBatchPerTick`
- 必要时可以带时间预算,例如每帧不超过 `X ms`
这样可避免同一帧大量 `Destroy`
### 11.4 低内存策略
当收到 `Application.lowMemory` 时:
- 忽略部分 `minRetained`
- 优先回收低优先级池
- 优先卸载不常用 prefab
- 进入短时激进回收模式
---
## 12. 调度策略设计
当前全量扫描策略不适合作为最终版。
推荐:
- 用最小堆维护池的下次清理时间
- 池状态变化时只更新自己的 `nextDueTime`
- 调度器只处理到期池
- 清理和预热都按 budget 驱动
这样可以避免:
- 每次回收都扫描所有池
- 大量池同时存在时的调度浪费
---
## 13. 自动回收设计
推荐提供一组通用 helper / component
- `ReturnToPoolAfterSeconds`
- `ReturnToPoolOnParticleStopped`
- `ReturnToPoolOnAnimatorStateExit`
- `ReturnToPoolOnAudioFinished`
- `ReturnToPoolOnDistanceExceeded`
- `ReturnToPoolOnCollision`
目标:
- 业务不重复造轮子
- 自动回收行为可视化配置
- 减少“忘记 Release”导致的泄漏
---
## 14. 运行时统计设计
最优方案必须带统计闭环。
每个池建议统计:
- `acquireCount`
- `releaseCount`
- `hitCount`
- `missCount`
- `peakActive`
- `peakTotal`
- `expandCount`
- `exhaustedCount`
- `autoRecycleCount`
- `destroyCount`
- `avgLifetime`
- `avgIdleTime`
- `avgAcquireCostMs`
- `avgReleaseCostMs`
调试用途:
- Inspector 面板
- 运行时快照
- CSV / JSON 导出
- 自动生成调优建议
---
## 15. 推荐默认配置区间
以下是默认建议值,不是硬编码结论,应由运行时统计修正。
### 15.1 通用默认
- `minRetained = 2 ~ 8`
- `softCapacity = 8 ~ 32`
- `hardCapacity = softCapacity * 1.5 ~ 2`
- `idleTrimDelay = 15s ~ 60s`
- `prefabUnloadDelay = 60s ~ 300s`
- `trimBatchPerTick = 1 ~ 4`
### 15.2 子弹默认
- `minRetained = 16`
- `softCapacity = 64`
- `hardCapacity = 128`
- `overflowPolicy = AutoExpand`
- `prefabUnloadDelay = 300s`
### 15.3 普通特效默认
- `minRetained = 4`
- `softCapacity = 16`
- `hardCapacity = 32`
- `overflowPolicy = DropNewestRequest`
- `idleTrimDelay = 20s`
### 15.4 怪物默认
- `minRetained = 2`
- `softCapacity = 8`
- `hardCapacity = 16`
- `overflowPolicy = AutoExpand`
- `activationMode = SleepWake`
---
## 16. 与当前实现的映射关系
当前模块中可保留的部分:
- `GameObjectPoolManager`
继续作为总入口和调度器雏形
- `RuntimePrefabPool`
继续作为单 prefab 池雏形
- `PoolConfig`
升级为新配置模型的一部分
- `IResourceLoader`
可继续作为资源加载抽象
需要重点重构的部分:
- 实例生命周期协议
- 池满策略
- 预热与清理预算调度
- 统计系统
- 自动回收组件
- 怪物/特效/子弹的分类默认策略
---
## 17. 迁移路线
建议分四个阶段落地。
### 阶段 1补生命周期协议
目标:
- 引入 `IGameObjectPoolable`
- Acquire / Release 时统一回调
- 允许对象自定义重置
### 阶段 2补配置模型
目标:
- 引入 `PoolProfile + PoolRule`
- 加入 `minRetained / softCapacity / hardCapacity / overflowPolicy`
- 兼容旧 `PoolConfig`
### 阶段 3补自动回收与预算调度
目标:
- 自动回收 helper
- 分帧 warmup
- 预算式 trim
- 低内存模式
### 阶段 4补统计与业务门面
目标:
- 运行时统计面板
- 容量推荐导出
- `EnemyPool / BulletPool / FxPool` 业务门面
---
## 18. 最终结论
最优对象池方案不是单一 `GameObject` 缓存容器,而是:
- 以 prefab 池为底层
- 以生命周期协议为核心
- 以分类策略为手段
- 以预算调度为性能保障
- 以统计闭环为调优依据
如果后续按本文档实施,建议优先级如下:
1. `IGameObjectPoolable``PoolSpawnContext`
2. `PoolProfile + PoolRule`
3. `PoolOverflowPolicy`
4. 自动回收 helper
5. 分帧预热与预算清理
6. 运行时统计与调优导出

View File

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: c1c2737cf20d46c7814c0552b7f0720d
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant: