11
This commit is contained in:
parent
9d75547998
commit
c4c9a22fc4
@ -80,13 +80,26 @@ namespace AlicizaX
|
|||||||
|
|
||||||
if (_foldoutState[key])
|
if (_foldoutState[key])
|
||||||
{
|
{
|
||||||
EditorGUILayout.LabelField("Match Mode", snapshot.matchMode.ToString());
|
EditorGUILayout.LabelField("Entry", snapshot.entryName);
|
||||||
EditorGUILayout.LabelField("Loader", snapshot.loaderType.ToString());
|
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("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 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)
|
if (snapshot.instances.Count > 0)
|
||||||
{
|
{
|
||||||
@ -114,6 +127,7 @@ namespace AlicizaX
|
|||||||
EditorGUILayout.BeginVertical();
|
EditorGUILayout.BeginVertical();
|
||||||
EditorGUILayout.LabelField(instance.instanceName, EditorStyles.boldLabel);
|
EditorGUILayout.LabelField(instance.instanceName, EditorStyles.boldLabel);
|
||||||
EditorGUILayout.LabelField("State", instance.isActive ? "Active" : "Inactive");
|
EditorGUILayout.LabelField("State", instance.isActive ? "Active" : "Inactive");
|
||||||
|
EditorGUILayout.LabelField("Life", $"{instance.lifeDuration:F1}s");
|
||||||
if (!instance.isActive)
|
if (!instance.isActive)
|
||||||
{
|
{
|
||||||
EditorGUILayout.LabelField("Idle", $"{instance.idleDuration:F1}s");
|
EditorGUILayout.LabelField("Idle", $"{instance.idleDuration:F1}s");
|
||||||
|
|||||||
596
Editor/GameObjectPool/PoolConfigEditorWindow.cs
Normal file
596
Editor/GameObjectPool/PoolConfigEditorWindow.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
11
Editor/GameObjectPool/PoolConfigEditorWindow.cs.meta
Normal file
11
Editor/GameObjectPool/PoolConfigEditorWindow.cs.meta
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 3c4eefe489788e84ba9ec7a3c27daad2
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@ -1,6 +1,4 @@
|
|||||||
using System.Collections.Generic;
|
|
||||||
using UnityEditor;
|
using UnityEditor;
|
||||||
using UnityEditorInternal;
|
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
|
|
||||||
namespace AlicizaX
|
namespace AlicizaX
|
||||||
@ -8,173 +6,12 @@ namespace AlicizaX
|
|||||||
[CustomEditor(typeof(PoolConfigScriptableObject))]
|
[CustomEditor(typeof(PoolConfigScriptableObject))]
|
||||||
public sealed class PoolConfigScriptableObjectEditor : UnityEditor.Editor
|
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()
|
public override void OnInspectorGUI()
|
||||||
{
|
{
|
||||||
serializedObject.Update();
|
if (GUILayout.Button("Open Editor", GUILayout.Width(160f), GUILayout.Height(28f)))
|
||||||
|
|
||||||
EditorGUILayout.HelpBox(
|
|
||||||
"每条配置定义一条匹配规则;真正的池按具体 assetPath 实例化,不再共享一个目录级总容量。",
|
|
||||||
MessageType.Info);
|
|
||||||
|
|
||||||
EditorGUILayout.BeginHorizontal();
|
|
||||||
if (GUILayout.Button("Normalize"))
|
|
||||||
{
|
{
|
||||||
serializedObject.ApplyModifiedProperties();
|
PoolConfigEditorWindow.Open((PoolConfigScriptableObject)target);
|
||||||
NormalizeAndSort(shouldSort: false);
|
|
||||||
serializedObject.Update();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
8
Runtime/ABase/GameObjectPool/Components.meta
Normal file
8
Runtime/ABase/GameObjectPool/Components.meta
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: f4ebb80ad5f287f42b3b4993abafba1b
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
308
Runtime/ABase/GameObjectPool/Components/PoolComponents.cs
Normal file
308
Runtime/ABase/GameObjectPool/Components/PoolComponents.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: fb76659ea7bc2ee4cba827951180c766
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@ -1,6 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
using UnityEngine.Serialization;
|
|
||||||
|
|
||||||
namespace AlicizaX
|
namespace AlicizaX
|
||||||
{
|
{
|
||||||
@ -16,49 +16,101 @@ namespace AlicizaX
|
|||||||
Prefix = 1
|
Prefix = 1
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
public enum PoolOverflowPolicy
|
||||||
/// 对象池配置项。
|
{
|
||||||
/// </summary>
|
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]
|
[Serializable]
|
||||||
public sealed class PoolConfig
|
public sealed class PoolEntry
|
||||||
{
|
{
|
||||||
public const string DefaultGroup = "Default";
|
public const string DefaultGroup = "Default";
|
||||||
public const int DefaultCapacity = 8;
|
public const string DefaultEntryName = "DefaultPool";
|
||||||
public const float DefaultInstanceIdleTimeout = 30f;
|
|
||||||
public const float DefaultPrefabUnloadDelay = 60f;
|
|
||||||
|
|
||||||
|
public string entryName = DefaultEntryName;
|
||||||
public string group = DefaultGroup;
|
public string group = DefaultGroup;
|
||||||
|
|
||||||
[FormerlySerializedAs("asset")]
|
|
||||||
public string assetPath;
|
public string assetPath;
|
||||||
|
|
||||||
public PoolMatchMode matchMode = PoolMatchMode.Exact;
|
public PoolMatchMode matchMode = PoolMatchMode.Exact;
|
||||||
public PoolResourceLoaderType resourceLoaderType = PoolResourceLoaderType.AssetBundle;
|
public PoolResourceLoaderType loaderType = PoolResourceLoaderType.AssetBundle;
|
||||||
|
public PoolOverflowPolicy overflowPolicy = PoolOverflowPolicy.FailFast;
|
||||||
[FormerlySerializedAs("time")]
|
public PoolTrimPolicy trimPolicy = PoolTrimPolicy.IdleOnly;
|
||||||
[Min(0f)]
|
public PoolActivationMode activationMode = PoolActivationMode.SetActive;
|
||||||
public float instanceIdleTimeout = DefaultInstanceIdleTimeout;
|
public PoolResetMode resetMode = PoolResetMode.PoolableCallbacks;
|
||||||
|
|
||||||
[Min(0f)]
|
|
||||||
public float prefabUnloadDelay = DefaultPrefabUnloadDelay;
|
|
||||||
|
|
||||||
[FormerlySerializedAs("poolCount")]
|
|
||||||
[Min(1)]
|
|
||||||
public int capacity = DefaultCapacity;
|
|
||||||
|
|
||||||
[Min(0)]
|
[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()
|
public void Normalize()
|
||||||
{
|
{
|
||||||
|
entryName = string.IsNullOrWhiteSpace(entryName) ? DefaultEntryName : entryName.Trim();
|
||||||
group = string.IsNullOrWhiteSpace(group) ? DefaultGroup : group.Trim();
|
group = string.IsNullOrWhiteSpace(group) ? DefaultGroup : group.Trim();
|
||||||
assetPath = NormalizeAssetPath(assetPath);
|
assetPath = NormalizeAssetPath(assetPath);
|
||||||
capacity = Mathf.Max(1, capacity);
|
minRetained = Mathf.Max(0, minRetained);
|
||||||
prewarmCount = Mathf.Clamp(prewarmCount, 0, capacity);
|
softCapacity = Mathf.Max(1, softCapacity);
|
||||||
instanceIdleTimeout = Mathf.Max(0f, instanceIdleTimeout);
|
hardCapacity = Mathf.Max(softCapacity, hardCapacity);
|
||||||
|
minRetained = Mathf.Min(minRetained, hardCapacity);
|
||||||
|
idleTrimDelay = Mathf.Max(0f, idleTrimDelay);
|
||||||
prefabUnloadDelay = Mathf.Max(0f, prefabUnloadDelay);
|
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)
|
public bool Matches(string requestedAssetPath, string requestedGroup = null)
|
||||||
@ -84,10 +136,11 @@ namespace AlicizaX
|
|||||||
|
|
||||||
public string BuildResolvedPoolKey(string resolvedAssetPath)
|
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))
|
if (ReferenceEquals(left, right))
|
||||||
{
|
{
|
||||||
@ -104,13 +157,21 @@ namespace AlicizaX
|
|||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int priorityCompare = right.priority.CompareTo(left.priority);
|
||||||
|
if (priorityCompare != 0)
|
||||||
|
{
|
||||||
|
return priorityCompare;
|
||||||
|
}
|
||||||
|
|
||||||
int modeCompare = left.matchMode.CompareTo(right.matchMode);
|
int modeCompare = left.matchMode.CompareTo(right.matchMode);
|
||||||
if (modeCompare != 0)
|
if (modeCompare != 0)
|
||||||
{
|
{
|
||||||
return modeCompare;
|
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)
|
if (pathLengthCompare != 0)
|
||||||
{
|
{
|
||||||
return pathLengthCompare;
|
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));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,3 +1,11 @@
|
|||||||
fileFormatVersion: 2
|
fileFormatVersion: 2
|
||||||
guid: 7de4b0d73b4145b6bd836ec9d2c832d9
|
guid: 7de4b0d73b4145b6bd836ec9d2c832d9
|
||||||
timeCreated: 1774439841
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
|
|||||||
@ -6,19 +6,38 @@ namespace AlicizaX
|
|||||||
[CreateAssetMenu(fileName = "PoolConfig", menuName = "GameplaySystem/PoolConfig", order = 10)]
|
[CreateAssetMenu(fileName = "PoolConfig", menuName = "GameplaySystem/PoolConfig", order = 10)]
|
||||||
public class PoolConfigScriptableObject : ScriptableObject
|
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()
|
public void Normalize()
|
||||||
{
|
{
|
||||||
if (configs == null)
|
if (entries == null)
|
||||||
{
|
{
|
||||||
configs = new List<PoolConfig>();
|
entries = new List<PoolEntry>();
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -18,22 +18,18 @@ namespace AlicizaX
|
|||||||
return groupCompare != 0 ? groupCompare : string.Compare(left.assetPath, right.assetPath, StringComparison.Ordinal);
|
return groupCompare != 0 ? groupCompare : string.Compare(left.assetPath, right.assetPath, StringComparison.Ordinal);
|
||||||
};
|
};
|
||||||
|
|
||||||
[Header("检查间隔")] public float checkInterval = 10f;
|
[Header("Cleanup Interval")] public float checkInterval = 5f;
|
||||||
|
[Header("Config Path")] public string poolConfigPath = "Assets/Bundles/Configs/PoolConfig";
|
||||||
[Header("配置路径")] public string poolConfigPath = "Assets/Bundles/Configs/PoolConfig";
|
[Header("Show Detailed Info")] public bool showDetailedInfo = true;
|
||||||
|
|
||||||
[Header("Inspector显示设置")] public bool showDetailedInfo = true;
|
|
||||||
|
|
||||||
[SerializeField] internal Transform poolContainer;
|
[SerializeField] internal Transform poolContainer;
|
||||||
|
|
||||||
private const PoolResourceLoaderType DefaultDirectLoadResourceLoaderType = PoolResourceLoaderType.AssetBundle;
|
private const PoolResourceLoaderType DefaultDirectLoadResourceLoaderType = PoolResourceLoaderType.AssetBundle;
|
||||||
|
|
||||||
private readonly Dictionary<string, RuntimePrefabPool> _poolsByKey = new Dictionary<string, RuntimePrefabPool>(StringComparer.Ordinal);
|
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, ResolvedPoolConfig> _resolvedConfigCache = new Dictionary<string, ResolvedPoolConfig>(StringComparer.Ordinal);
|
||||||
private readonly Dictionary<string, PoolResourceLoaderType> _groupLoaderCache = new Dictionary<string, PoolResourceLoaderType>(StringComparer.Ordinal);
|
private readonly Dictionary<string, RuntimePrefabPool> _ownersByObject = new Dictionary<string, RuntimePrefabPool>(StringComparer.Ordinal);
|
||||||
private readonly Dictionary<GameObject, RuntimePrefabPool> _ownersByObject = new Dictionary<GameObject, RuntimePrefabPool>();
|
|
||||||
private readonly Dictionary<PoolResourceLoaderType, IResourceLoader> _resourceLoaders = new Dictionary<PoolResourceLoaderType, IResourceLoader>();
|
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 readonly List<GameObjectPoolSnapshot> _debugSnapshots = new List<GameObjectPoolSnapshot>();
|
||||||
|
|
||||||
private CancellationTokenSource _shutdownTokenSource;
|
private CancellationTokenSource _shutdownTokenSource;
|
||||||
@ -42,8 +38,8 @@ namespace AlicizaX
|
|||||||
private Exception _initializationException;
|
private Exception _initializationException;
|
||||||
private bool _cleanupScheduled;
|
private bool _cleanupScheduled;
|
||||||
private float _nextCleanupTime = float.MaxValue;
|
private float _nextCleanupTime = float.MaxValue;
|
||||||
|
|
||||||
private bool _isShuttingDown;
|
private bool _isShuttingDown;
|
||||||
|
private bool _aggressiveCleanupRequested;
|
||||||
|
|
||||||
public bool IsReady => _initializationCompleted && _initializationException == null;
|
public bool IsReady => _initializationCompleted && _initializationException == null;
|
||||||
|
|
||||||
@ -80,7 +76,6 @@ namespace AlicizaX
|
|||||||
await UniTask.WaitUntil(() => YooAsset.YooAssets.Initialized, cancellationToken: cancellationToken);
|
await UniTask.WaitUntil(() => YooAsset.YooAssets.Initialized, cancellationToken: cancellationToken);
|
||||||
LoadConfigs();
|
LoadConfigs();
|
||||||
_initializationCompleted = true;
|
_initializationCompleted = true;
|
||||||
await PrewarmConfiguredPoolsAsync(cancellationToken);
|
|
||||||
ScheduleCleanup();
|
ScheduleCleanup();
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException)
|
catch (OperationCanceledException)
|
||||||
@ -107,7 +102,8 @@ namespace AlicizaX
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
PerformCleanup();
|
PerformCleanup(_aggressiveCleanupRequested);
|
||||||
|
_aggressiveCleanupRequested = false;
|
||||||
ScheduleCleanup();
|
ScheduleCleanup();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -126,35 +122,161 @@ namespace AlicizaX
|
|||||||
_resourceLoaders[loaderType] = resourceLoader;
|
_resourceLoaders[loaderType] = resourceLoader;
|
||||||
}
|
}
|
||||||
|
|
||||||
public GameObject GetGameObject(string assetPath, Transform parent = null)
|
public GameObject GetGameObject(string assetPath, Transform parent = null, object userData = null)
|
||||||
{
|
{
|
||||||
EnsureReadyForSyncUse();
|
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();
|
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(
|
public async UniTask<GameObject> GetGameObjectAsync(
|
||||||
string assetPath,
|
string assetPath,
|
||||||
Transform parent = null,
|
Transform parent = null,
|
||||||
|
object userData = null,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
await EnsureInitializedAsync(cancellationToken);
|
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(
|
public async UniTask<GameObject> GetGameObjectAsyncByGroup(
|
||||||
string group,
|
string group,
|
||||||
string assetPath,
|
string assetPath,
|
||||||
Transform parent = null,
|
Transform parent = null,
|
||||||
|
object userData = null,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
await EnsureInitializedAsync(cancellationToken);
|
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)
|
public void Release(GameObject gameObject)
|
||||||
@ -164,7 +286,8 @@ namespace AlicizaX
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_ownersByObject.TryGetValue(gameObject, out RuntimePrefabPool pool))
|
string instanceKey = GetInstanceKey(gameObject);
|
||||||
|
if (_ownersByObject.TryGetValue(instanceKey, out RuntimePrefabPool pool))
|
||||||
{
|
{
|
||||||
pool.Release(gameObject);
|
pool.Release(gameObject);
|
||||||
return;
|
return;
|
||||||
@ -174,22 +297,10 @@ namespace AlicizaX
|
|||||||
Destroy(gameObject);
|
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)
|
public async UniTask PreloadAsync(string assetPath, int count = 1, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
await EnsureInitializedAsync(cancellationToken);
|
await EnsureInitializedAsync(cancellationToken);
|
||||||
await PreloadInternalAsync(PoolConfig.NormalizeAssetPath(assetPath), null, count, cancellationToken);
|
await PreloadInternalAsync(PoolEntry.NormalizeAssetPath(assetPath), null, count, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async UniTask PreloadAsyncByGroup(
|
public async UniTask PreloadAsyncByGroup(
|
||||||
@ -199,7 +310,7 @@ namespace AlicizaX
|
|||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
await EnsureInitializedAsync(cancellationToken);
|
await EnsureInitializedAsync(cancellationToken);
|
||||||
await PreloadInternalAsync(PoolConfig.NormalizeAssetPath(assetPath), group, count, cancellationToken);
|
await PreloadInternalAsync(PoolEntry.NormalizeAssetPath(assetPath), group, count, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void ForceCleanup()
|
public void ForceCleanup()
|
||||||
@ -209,7 +320,7 @@ namespace AlicizaX
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
PerformCleanup();
|
PerformCleanup(false);
|
||||||
ScheduleCleanup();
|
ScheduleCleanup();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -223,11 +334,10 @@ namespace AlicizaX
|
|||||||
}
|
}
|
||||||
|
|
||||||
_isShuttingDown = false;
|
_isShuttingDown = false;
|
||||||
|
|
||||||
_poolsByKey.Clear();
|
_poolsByKey.Clear();
|
||||||
_ownersByObject.Clear();
|
_ownersByObject.Clear();
|
||||||
_resolvedConfigCache.Clear();
|
_resolvedConfigCache.Clear();
|
||||||
_groupLoaderCache.Clear();
|
_entries.Clear();
|
||||||
ReleaseDebugSnapshots();
|
ReleaseDebugSnapshots();
|
||||||
_cleanupScheduled = false;
|
_cleanupScheduled = false;
|
||||||
_nextCleanupTime = float.MaxValue;
|
_nextCleanupTime = float.MaxValue;
|
||||||
@ -244,7 +354,6 @@ namespace AlicizaX
|
|||||||
}
|
}
|
||||||
|
|
||||||
_debugSnapshots.Sort(SnapshotComparer);
|
_debugSnapshots.Sort(SnapshotComparer);
|
||||||
|
|
||||||
return _debugSnapshots;
|
return _debugSnapshots;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -255,7 +364,7 @@ namespace AlicizaX
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
_ownersByObject[gameObject] = pool;
|
_ownersByObject[GetInstanceKey(gameObject)] = pool;
|
||||||
NotifyPoolStateChanged();
|
NotifyPoolStateChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -266,7 +375,7 @@ namespace AlicizaX
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
_ownersByObject.Remove(gameObject);
|
_ownersByObject.Remove(GetInstanceKey(gameObject));
|
||||||
NotifyPoolStateChanged();
|
NotifyPoolStateChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -280,50 +389,32 @@ namespace AlicizaX
|
|||||||
ScheduleCleanup();
|
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)
|
if (config == null)
|
||||||
{
|
{
|
||||||
return LoadUnpooled(assetPath, group, parent);
|
return LoadUnpooled(assetPath, group, context.Parent);
|
||||||
}
|
}
|
||||||
|
|
||||||
RuntimePrefabPool pool = GetOrCreatePool(config, assetPath);
|
RuntimePrefabPool pool = GetOrCreatePool(config, assetPath);
|
||||||
return pool.Acquire(parent);
|
return pool.Acquire(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async UniTask<GameObject> GetGameObjectInternalAsync(
|
private async UniTask<GameObject> GetGameObjectInternalAsync(
|
||||||
string assetPath,
|
string assetPath,
|
||||||
string group,
|
string group,
|
||||||
Transform parent,
|
PoolSpawnContext context,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
PoolConfig config = ResolveConfig(assetPath, group);
|
ResolvedPoolConfig config = ResolveConfig(assetPath, group);
|
||||||
if (config == null)
|
if (config == null)
|
||||||
{
|
{
|
||||||
return await LoadUnpooledAsync(assetPath, group, parent, cancellationToken);
|
return await LoadUnpooledAsync(assetPath, group, context.Parent, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
RuntimePrefabPool pool = GetOrCreatePool(config, assetPath);
|
RuntimePrefabPool pool = GetOrCreatePool(config, assetPath);
|
||||||
return await pool.AcquireAsync(parent, cancellationToken);
|
return await pool.AcquireAsync(context, 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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async UniTask PreloadInternalAsync(
|
private async UniTask PreloadInternalAsync(
|
||||||
@ -337,10 +428,10 @@ namespace AlicizaX
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
PoolConfig config = ResolveConfig(assetPath, group);
|
ResolvedPoolConfig config = ResolveConfig(assetPath, group);
|
||||||
if (config == null)
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -348,7 +439,7 @@ namespace AlicizaX
|
|||||||
await pool.WarmupAsync(count, cancellationToken);
|
await pool.WarmupAsync(count, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
private RuntimePrefabPool GetOrCreatePool(PoolConfig config, string assetPath)
|
private RuntimePrefabPool GetOrCreatePool(ResolvedPoolConfig config, string assetPath)
|
||||||
{
|
{
|
||||||
EnsurePoolContainer();
|
EnsurePoolContainer();
|
||||||
|
|
||||||
@ -362,7 +453,7 @@ namespace AlicizaX
|
|||||||
pool.Initialize(
|
pool.Initialize(
|
||||||
config,
|
config,
|
||||||
assetPath,
|
assetPath,
|
||||||
GetResourceLoader(config.resourceLoaderType),
|
GetResourceLoader(config.loaderType),
|
||||||
this,
|
this,
|
||||||
_shutdownTokenSource != null ? _shutdownTokenSource.Token : default);
|
_shutdownTokenSource != null ? _shutdownTokenSource.Token : default);
|
||||||
|
|
||||||
@ -371,12 +462,11 @@ namespace AlicizaX
|
|||||||
return pool;
|
return pool;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void PerformCleanup()
|
private void PerformCleanup(bool aggressive)
|
||||||
{
|
{
|
||||||
float now = Time.time;
|
|
||||||
foreach (RuntimePrefabPool pool in _poolsByKey.Values)
|
foreach (RuntimePrefabPool pool in _poolsByKey.Values)
|
||||||
{
|
{
|
||||||
pool.TrimExpiredInstances(now);
|
pool.Trim(aggressive ? int.MaxValue : 0, aggressive);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -390,20 +480,21 @@ namespace AlicizaX
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
float nextDueTime = float.MaxValue;
|
|
||||||
float now = Time.time;
|
float now = Time.time;
|
||||||
|
bool hasWork = false;
|
||||||
|
float nextDueTime = now + Mathf.Max(checkInterval, 0.1f);
|
||||||
foreach (RuntimePrefabPool pool in _poolsByKey.Values)
|
foreach (RuntimePrefabPool pool in _poolsByKey.Values)
|
||||||
{
|
{
|
||||||
float candidate = pool.GetNextCleanupTime(now, checkInterval);
|
if (pool.NeedsTrim(now) || pool.NeedsPrefabUnload(now, false))
|
||||||
if (candidate < nextDueTime)
|
|
||||||
{
|
{
|
||||||
nextDueTime = candidate;
|
hasWork = true;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_nextCleanupTime = nextDueTime;
|
_nextCleanupTime = hasWork ? nextDueTime : float.MaxValue;
|
||||||
_cleanupScheduled = nextDueTime < float.MaxValue;
|
_cleanupScheduled = hasWork;
|
||||||
enabled = _cleanupScheduled;
|
enabled = hasWork;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnLowMemory()
|
private void OnLowMemory()
|
||||||
@ -413,7 +504,8 @@ namespace AlicizaX
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
PerformCleanup();
|
_aggressiveCleanupRequested = true;
|
||||||
|
PerformCleanup(true);
|
||||||
ScheduleCleanup();
|
ScheduleCleanup();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -444,117 +536,50 @@ namespace AlicizaX
|
|||||||
|
|
||||||
private void LoadConfigs()
|
private void LoadConfigs()
|
||||||
{
|
{
|
||||||
_configs.Clear();
|
_entries.Clear();
|
||||||
_resolvedConfigCache.Clear();
|
_resolvedConfigCache.Clear();
|
||||||
_groupLoaderCache.Clear();
|
|
||||||
|
|
||||||
IResourceService resourceService = AppServices.Require<IResourceService>();
|
IResourceService resourceService = AppServices.Require<IResourceService>();
|
||||||
PoolConfigScriptableObject configAsset = resourceService.LoadAsset<PoolConfigScriptableObject>(poolConfigPath);
|
PoolConfigScriptableObject configAsset = resourceService.LoadAsset<PoolConfigScriptableObject>(poolConfigPath);
|
||||||
|
|
||||||
if (configAsset == null || configAsset.configs == null)
|
if (configAsset == null)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (int i = 0; i < configAsset.configs.Count; i++)
|
PoolConfigCatalog catalog = configAsset.BuildCatalog();
|
||||||
{
|
_entries.AddRange(catalog.entries);
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
resourceService.UnloadAsset(configAsset);
|
resourceService.UnloadAsset(configAsset);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async UniTask PrewarmConfiguredPoolsAsync(CancellationToken cancellationToken)
|
private ResolvedPoolConfig ResolveConfig(string assetPath, string group)
|
||||||
{
|
|
||||||
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)
|
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(assetPath))
|
if (string.IsNullOrWhiteSpace(assetPath))
|
||||||
{
|
{
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool hasGroup = !string.IsNullOrEmpty(group);
|
string cacheKey = string.IsNullOrWhiteSpace(group) ? assetPath : string.Concat(group, "|", assetPath);
|
||||||
string cacheKey = hasGroup ? string.Concat(group, "|", assetPath) : assetPath;
|
if (_resolvedConfigCache.TryGetValue(cacheKey, out ResolvedPoolConfig cachedConfig))
|
||||||
if (_resolvedConfigCache.TryGetValue(cacheKey, out PoolConfig cachedConfig))
|
|
||||||
{
|
{
|
||||||
return cachedConfig;
|
return cachedConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
PoolConfig matchedConfig = null;
|
for (int i = 0; i < _entries.Count; i++)
|
||||||
for (int i = 0; i < _configs.Count; i++)
|
|
||||||
{
|
{
|
||||||
PoolConfig candidate = _configs[i];
|
PoolEntry entry = _entries[i];
|
||||||
if (!candidate.Matches(assetPath, group))
|
if (!entry.Matches(assetPath, group))
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (matchedConfig == null)
|
ResolvedPoolConfig resolved = ResolvedPoolConfig.From(entry);
|
||||||
{
|
_resolvedConfigCache[cacheKey] = resolved;
|
||||||
matchedConfig = candidate;
|
return resolved;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_resolvedConfigCache[cacheKey] = matchedConfig;
|
_resolvedConfigCache[cacheKey] = null;
|
||||||
return matchedConfig;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private GameObject LoadUnpooled(string assetPath, string group, Transform parent)
|
private GameObject LoadUnpooled(string assetPath, string group, Transform parent)
|
||||||
@ -575,10 +600,16 @@ namespace AlicizaX
|
|||||||
|
|
||||||
private IResourceLoader GetDirectLoadResourceLoader(string group)
|
private IResourceLoader GetDirectLoadResourceLoader(string group)
|
||||||
{
|
{
|
||||||
if (!string.IsNullOrWhiteSpace(group) &&
|
if (!string.IsNullOrWhiteSpace(group))
|
||||||
_groupLoaderCache.TryGetValue(group, out PoolResourceLoaderType loaderType))
|
|
||||||
{
|
{
|
||||||
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);
|
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()
|
private void ReleaseDebugSnapshots()
|
||||||
{
|
{
|
||||||
for (int i = 0; i < _debugSnapshots.Count; i++)
|
for (int i = 0; i < _debugSnapshots.Count; i++)
|
||||||
@ -641,5 +658,10 @@ namespace AlicizaX
|
|||||||
|
|
||||||
_debugSnapshots.Clear();
|
_debugSnapshots.Clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static string GetInstanceKey(GameObject gameObject)
|
||||||
|
{
|
||||||
|
return gameObject.GetInstanceID().ToString();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
722
Runtime/ABase/GameObjectPool/fe.md
Normal file
722
Runtime/ABase/GameObjectPool/fe.md
Normal 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. 运行时统计与调优导出
|
||||||
|
|
||||||
7
Runtime/ABase/GameObjectPool/fe.md.meta
Normal file
7
Runtime/ABase/GameObjectPool/fe.md.meta
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: c1c2737cf20d46c7814c0552b7f0720d
|
||||||
|
TextScriptImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
Loading…
Reference in New Issue
Block a user