[Opt]重构GameObjectPool

This commit is contained in:
陈思海 2026-04-30 17:18:17 +08:00
parent 03f479dc0e
commit 49353c6d61
8 changed files with 2705 additions and 1749 deletions

View File

@ -1,3 +1,4 @@
using System;
using System.Collections.Generic; using System.Collections.Generic;
using UnityEditor; using UnityEditor;
using UnityEngine; using UnityEngine;
@ -7,49 +8,57 @@ namespace AlicizaX
[CustomEditor(typeof(GameObjectPoolManager))] [CustomEditor(typeof(GameObjectPoolManager))]
public sealed class GameObjectPoolEditor : UnityEditor.Editor public sealed class GameObjectPoolEditor : UnityEditor.Editor
{ {
private readonly Dictionary<string, bool> _foldoutState = new Dictionary<string, bool>(); private readonly Dictionary<string, bool> _foldoutState = new Dictionary<string, bool>(StringComparer.Ordinal);
public override void OnInspectorGUI() public override void OnInspectorGUI()
{ {
serializedObject.Update(); serializedObject.Update();
DrawDefaultInspector(); DrawDefaultInspector();
serializedObject.ApplyModifiedProperties(); serializedObject.ApplyModifiedProperties();
var pool = (GameObjectPoolManager)target; var poolManager = (GameObjectPoolManager)target;
if (!Application.isPlaying) if (!Application.isPlaying)
{ {
EditorGUILayout.HelpBox("Enter Play Mode to inspect runtime pool state.", MessageType.Info); EditorGUILayout.HelpBox("Enter Play Mode to inspect the runtime pool.", MessageType.Info);
return; return;
} }
if (!pool.showDetailedInfo) if (!poolManager.showDetailedInfo)
{ {
return; return;
} }
EditorGUILayout.Space(); EditorGUILayout.Space();
DrawRuntimeState(pool); DrawRuntimeState(poolManager);
} }
public override bool RequiresConstantRepaint() public override bool RequiresConstantRepaint()
{ {
var pool = target as GameObjectPoolManager; var poolManager = target as GameObjectPoolManager;
return pool != null && Application.isPlaying && pool.showDetailedInfo; return poolManager != null && Application.isPlaying && poolManager.showDetailedInfo;
} }
private void DrawRuntimeState(GameObjectPoolManager poolManager) private void DrawRuntimeState(GameObjectPoolManager poolManager)
{ {
if (!poolManager.IsReady) GameObjectPoolSummarySnapshot summary = poolManager.GetDebugSummary();
DrawSummary(summary);
if (summary.WaitingForBootstrap)
{ {
EditorGUILayout.HelpBox("GameObjectPool is initializing.", MessageType.Info); EditorGUILayout.HelpBox("Waiting for YooAssets bootstrap before pool catalog build.", MessageType.Info);
return;
}
if (!summary.IsReady)
{
EditorGUILayout.HelpBox("Pool manager is not ready.", MessageType.Warning);
return; return;
} }
List<GameObjectPoolSnapshot> snapshots = poolManager.GetDebugSnapshots(); List<GameObjectPoolSnapshot> snapshots = poolManager.GetDebugSnapshots();
if (snapshots.Count == 0) if (snapshots.Count == 0)
{ {
EditorGUILayout.HelpBox("No runtime pools have been created yet.", MessageType.Info); EditorGUILayout.HelpBox("No pooled runtime instances exist yet.", MessageType.Info);
return; return;
} }
@ -59,6 +68,22 @@ namespace AlicizaX
} }
} }
private static void DrawSummary(in GameObjectPoolSummarySnapshot summary)
{
EditorGUILayout.BeginVertical("box");
EditorGUILayout.LabelField("Runtime Summary", EditorStyles.boldLabel);
EditorGUILayout.LabelField("Ready", summary.IsReady ? "Yes" : "No");
EditorGUILayout.LabelField("Waiting Bootstrap", summary.WaitingForBootstrap ? "Yes" : "No");
EditorGUILayout.LabelField("Pools", summary.PoolCount.ToString());
EditorGUILayout.LabelField("Loaded Prefabs", summary.LoadedPrefabCount.ToString());
EditorGUILayout.LabelField("Instances", summary.TotalInstanceCount.ToString());
EditorGUILayout.LabelField("Active", summary.ActiveInstanceCount.ToString());
EditorGUILayout.LabelField("Inactive", summary.InactiveInstanceCount.ToString());
EditorGUILayout.LabelField("Pending Maintenance", summary.PendingMaintenanceCount.ToString());
EditorGUILayout.EndVertical();
EditorGUILayout.Space();
}
private void DrawSnapshot(GameObjectPoolSnapshot snapshot) private void DrawSnapshot(GameObjectPoolSnapshot snapshot)
{ {
if (snapshot == null) if (snapshot == null)
@ -66,40 +91,49 @@ namespace AlicizaX
return; return;
} }
string key = $"{snapshot.group}|{snapshot.assetPath}"; string entryLabel = string.IsNullOrWhiteSpace(snapshot.entryName) ? snapshot.assetPath : snapshot.entryName;
if (!_foldoutState.ContainsKey(key)) string foldoutKey = string.Concat(entryLabel, "|", snapshot.assetPath);
if (!_foldoutState.ContainsKey(foldoutKey))
{ {
_foldoutState[key] = false; _foldoutState[foldoutKey] = false;
} }
EditorGUILayout.BeginVertical("box"); string foldoutLabel = string.Format(
_foldoutState[key] = EditorGUILayout.Foldout( "{0} [{1}/{2}] Hit:{3}/{4}",
_foldoutState[key], entryLabel,
$"{snapshot.group} | {snapshot.assetPath} ({snapshot.activeCount}/{snapshot.totalCount})", snapshot.activeCount,
true); snapshot.totalCount,
snapshot.hitCount,
snapshot.acquireCount);
if (_foldoutState[key]) EditorGUILayout.BeginVertical("box");
_foldoutState[foldoutKey] = EditorGUILayout.Foldout(_foldoutState[foldoutKey], foldoutLabel, true);
if (_foldoutState[foldoutKey])
{ {
EditorGUILayout.LabelField("Entry", snapshot.entryName); EditorGUILayout.LabelField("Rule", entryLabel);
EditorGUILayout.LabelField("Asset Path", snapshot.assetPath);
EditorGUILayout.LabelField("Loader", snapshot.loaderType.ToString()); EditorGUILayout.LabelField("Loader", snapshot.loaderType.ToString());
EditorGUILayout.LabelField("Overflow Policy", snapshot.overflowPolicy.ToString()); EditorGUILayout.LabelField("Retain Target", snapshot.retainTarget.ToString());
EditorGUILayout.LabelField("Min Retained", snapshot.minRetained.ToString());
EditorGUILayout.LabelField("Soft Capacity", snapshot.softCapacity.ToString()); EditorGUILayout.LabelField("Soft Capacity", snapshot.softCapacity.ToString());
EditorGUILayout.LabelField("Hard Capacity", snapshot.hardCapacity.ToString()); EditorGUILayout.LabelField("Capacity", snapshot.hardCapacity.ToString());
EditorGUILayout.LabelField("Runtime Capacity", snapshot.runtimeHardCapacity.ToString());
EditorGUILayout.LabelField("Inactive", snapshot.inactiveCount.ToString()); EditorGUILayout.LabelField("Inactive", snapshot.inactiveCount.ToString());
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.LabelField("Prefab Cold", FormatSeconds(snapshot.prefabIdleDuration));
EditorGUILayout.LabelField("Wake Count", snapshot.prefabWakeCount.ToString());
EditorGUILayout.LabelField("Wake Gap", snapshot.prefabWakeGap < 0f ? "N/A" : FormatSeconds(snapshot.prefabWakeGap));
EditorGUILayout.LabelField("Unload Delay", snapshot.prefabUnloadDelay < 0f ? "N/A" : FormatSeconds(snapshot.prefabUnloadDelay));
EditorGUILayout.LabelField("Next Maintenance", snapshot.nextMaintenanceIn < 0f ? "None" : FormatSeconds(snapshot.nextMaintenanceIn));
EditorGUILayout.Space(); EditorGUILayout.Space();
EditorGUILayout.LabelField("Acquire", snapshot.acquireCount.ToString()); EditorGUILayout.LabelField("Acquire", snapshot.acquireCount.ToString());
EditorGUILayout.LabelField("Release", snapshot.releaseCount.ToString()); EditorGUILayout.LabelField("Release", snapshot.releaseCount.ToString());
EditorGUILayout.LabelField("Hit", snapshot.hitCount.ToString()); EditorGUILayout.LabelField("Hit", snapshot.hitCount.ToString());
EditorGUILayout.LabelField("Miss", snapshot.missCount.ToString()); EditorGUILayout.LabelField("Miss", snapshot.missCount.ToString());
EditorGUILayout.LabelField("Expand", snapshot.expandCount.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("Destroy", snapshot.destroyCount.ToString());
EditorGUILayout.LabelField("Peak Active", snapshot.peakActive.ToString()); EditorGUILayout.LabelField("Peak Active", snapshot.peakActive.ToString());
EditorGUILayout.LabelField("Peak Total", snapshot.peakTotal.ToString()); EditorGUILayout.LabelField("Peak Active Short", snapshot.peakActiveShort.ToString());
EditorGUILayout.LabelField("Peak Active Long", snapshot.peakActiveLong.ToString());
if (snapshot.instances.Count > 0) if (snapshot.instances.Count > 0)
{ {
@ -116,25 +150,31 @@ namespace AlicizaX
EditorGUILayout.Space(); EditorGUILayout.Space();
} }
private static void DrawInstance(GameObjectPoolInstanceSnapshot instance) private static void DrawInstance(GameObjectPoolInstanceSnapshot snapshot)
{ {
if (instance == null) if (snapshot == null)
{ {
return; return;
} }
EditorGUILayout.BeginHorizontal("box"); EditorGUILayout.BeginHorizontal("box");
EditorGUILayout.BeginVertical(); EditorGUILayout.BeginVertical();
EditorGUILayout.LabelField(instance.instanceName, EditorStyles.boldLabel); EditorGUILayout.LabelField(snapshot.instanceName, EditorStyles.boldLabel);
EditorGUILayout.LabelField("State", instance.isActive ? "Active" : "Inactive"); EditorGUILayout.LabelField("State", snapshot.isActive ? "Active" : "Inactive");
EditorGUILayout.LabelField("Life", $"{instance.lifeDuration:F1}s"); EditorGUILayout.LabelField("Life", FormatSeconds(snapshot.lifeDuration));
if (!instance.isActive) if (!snapshot.isActive)
{ {
EditorGUILayout.LabelField("Idle", $"{instance.idleDuration:F1}s"); EditorGUILayout.LabelField("Idle", FormatSeconds(snapshot.idleDuration));
} }
EditorGUILayout.EndVertical(); EditorGUILayout.EndVertical();
EditorGUILayout.ObjectField(instance.gameObject, typeof(GameObject), true, GUILayout.Width(120)); EditorGUILayout.ObjectField(snapshot.gameObject, typeof(GameObject), true, GUILayout.Width(140f));
EditorGUILayout.EndHorizontal(); EditorGUILayout.EndHorizontal();
} }
private static string FormatSeconds(float seconds)
{
return seconds.ToString("F2") + "s";
}
} }
} }

View File

@ -3,6 +3,7 @@ using AlicizaX.Editor;
using UnityEditor; using UnityEditor;
using UnityEditor.Callbacks; using UnityEditor.Callbacks;
using UnityEditor.UIElements; using UnityEditor.UIElements;
using UnityEditorInternal;
using UnityEngine; using UnityEngine;
using UnityEngine.UIElements; using UnityEngine.UIElements;
@ -12,37 +13,47 @@ namespace AlicizaX
{ {
private const float MinLeftWidth = 240f; private const float MinLeftWidth = 240f;
private const float InitialLeftWidth = 300f; private const float InitialLeftWidth = 300f;
private const int ListItemHeight = 46; private const int ListItemHeight = 24;
private const string WindowTitle = "Pool Config Editor"; private const string WindowTitle = "对象池配置编辑器";
private static readonly Color LeftPanelColor = new Color(0.22f, 0.22f, 0.22f, 1f); 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 static readonly Color RightPanelColor = new Color(0.16f, 0.16f, 0.16f, 1f);
private static readonly Color DescriptionColor = new Color(0.72f, 0.72f, 0.72f, 1f);
private static readonly List<string> MatchModeOptions = new List<string> { "精确匹配", "前缀匹配" };
private static readonly List<string> LoaderTypeOptions = new List<string> { "AssetBundle", "Resources" };
[SerializeField]
private PoolConfigScriptableObject _asset; private PoolConfigScriptableObject _asset;
[SerializeField]
private string _assetGuid;
private SerializedObject _serializedObject; private SerializedObject _serializedObject;
private SerializedProperty _entriesProperty; private SerializedProperty _entriesProperty;
private readonly List<int> _entryIndices = new List<int>(); private readonly List<int> _entryIndices = new List<int>();
[SerializeField]
private int _selectedIndex; private int _selectedIndex;
[SerializeField]
private bool _hasUnsavedChanges; private bool _hasUnsavedChanges;
[SerializeField]
private Vector2 _entryListScrollPosition;
private ToolbarButton _saveButton; private ToolbarButton _saveButton;
private Label _titleLabel; private Label _titleLabel;
private VisualElement _leftPane; private VisualElement _leftPane;
private ListView _listView; private IMGUIContainer _entryListContainer;
private ReorderableList _entryList;
private ScrollView _detailScrollView; private ScrollView _detailScrollView;
private Label _detailTitleLabel; private Label _detailTitleLabel;
private VisualElement _detailFieldsContainer; private VisualElement _detailFieldsContainer;
private VisualElement _emptyContainer; private VisualElement _emptyContainer;
[MenuItem("AlicizaX/GameObjectPool/Open PoolConfig Editor")] private static void OpenForAsset(PoolConfigScriptableObject asset)
public static void OpenWindow()
{ {
Open(Selection.activeObject as PoolConfigScriptableObject); if (asset == null)
} {
return;
}
public static void Open(PoolConfigScriptableObject asset)
{
PoolConfigEditorWindow window = GetWindow<PoolConfigEditorWindow>(false, WindowTitle, true); PoolConfigEditorWindow window = GetWindow<PoolConfigEditorWindow>(false, WindowTitle, true);
window.minSize = new Vector2(920f, 560f); window.minSize = new Vector2(920f, 560f);
window.SetAsset(asset); window.SetAsset(asset);
@ -58,7 +69,7 @@ namespace AlicizaX
return false; return false;
} }
Open(asset); OpenForAsset(asset);
return true; return true;
} }
@ -66,15 +77,8 @@ namespace AlicizaX
{ {
titleContent = new GUIContent(WindowTitle, EditorGUIUtility.IconContent("ScriptableObject Icon").image); titleContent = new GUIContent(WindowTitle, EditorGUIUtility.IconContent("ScriptableObject Icon").image);
BuildUi(); BuildUi();
RestoreWindowState();
if (_asset == null && Selection.activeObject is PoolConfigScriptableObject selectedAsset) RefreshUi();
{
SetAsset(selectedAsset);
}
else
{
RefreshUi();
}
} }
private void OnEnable() private void OnEnable()
@ -85,22 +89,8 @@ namespace AlicizaX
BuildUi(); BuildUi();
} }
if (_asset == null && Selection.activeObject is PoolConfigScriptableObject selectedAsset) RestoreWindowState();
{ RefreshUi();
SetAsset(selectedAsset);
}
else
{
RefreshUi();
}
}
private void OnSelectionChange()
{
if (Selection.activeObject is PoolConfigScriptableObject selectedAsset && selectedAsset != _asset)
{
SetAsset(selectedAsset);
}
} }
private void BuildUi() private void BuildUi()
@ -113,7 +103,7 @@ namespace AlicizaX
_saveButton = new ToolbarButton(SaveAsset) _saveButton = new ToolbarButton(SaveAsset)
{ {
tooltip = "Save PoolConfig" tooltip = "保存当前 PoolConfig 配置"
}; };
_saveButton.Add(new Image _saveButton.Add(new Image
{ {
@ -161,63 +151,10 @@ namespace AlicizaX
private void BuildLeftPane() private void BuildLeftPane()
{ {
_listView = new ListView _entryListContainer = new IMGUIContainer(DrawEntryList);
{ _entryListContainer.style.flexGrow = 1f;
selectionType = SelectionType.Single, _entryListContainer.style.marginBottom = 4f;
virtualizationMethod = CollectionVirtualizationMethod.FixedHeight, _leftPane.Add(_entryListContainer);
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) private void BuildRightPane(VisualElement rightPane)
@ -230,7 +167,7 @@ namespace AlicizaX
justifyContent = Justify.Center justifyContent = Justify.Center
} }
}; };
_emptyContainer.Add(new HelpBox("No entry selected.", HelpBoxMessageType.Info)); _emptyContainer.Add(new HelpBox("当前没有选中任何规则。", HelpBoxMessageType.Info));
_detailScrollView = new ScrollView _detailScrollView = new ScrollView
{ {
@ -256,59 +193,53 @@ namespace AlicizaX
rightPane.Add(_detailScrollView); 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) private void SetAsset(PoolConfigScriptableObject asset)
{ {
_asset = asset; _asset = asset;
_assetGuid = GetAssetGuid(asset);
_selectedIndex = 0; _selectedIndex = 0;
_hasUnsavedChanges = false; _hasUnsavedChanges = false;
_entryListScrollPosition = Vector2.zero;
RebindAssetState();
RefreshUi();
}
private void RestoreWindowState()
{
if (_asset == null && !string.IsNullOrEmpty(_assetGuid))
{
string assetPath = AssetDatabase.GUIDToAssetPath(_assetGuid);
if (!string.IsNullOrEmpty(assetPath))
{
_asset = AssetDatabase.LoadAssetAtPath<PoolConfigScriptableObject>(assetPath);
}
}
else if (_asset != null)
{
_assetGuid = GetAssetGuid(_asset);
}
RebindAssetState();
}
private void RebindAssetState()
{
_entryList = null;
if (_asset == null) if (_asset == null)
{ {
_assetGuid = string.Empty;
_serializedObject = null; _serializedObject = null;
_entriesProperty = null; _entriesProperty = null;
} _entryIndices.Clear();
else return;
{
_asset.Normalize();
_serializedObject = new SerializedObject(_asset);
_entriesProperty = _serializedObject.FindProperty("entries");
RebuildEntryIndices();
ClampSelection();
} }
RefreshUi(); _asset.Normalize();
_serializedObject = new SerializedObject(_asset);
_entriesProperty = _serializedObject.FindProperty("entries");
RebuildEntryIndices();
ClampSelection();
} }
private void RefreshUi() private void RefreshUi()
@ -324,24 +255,23 @@ namespace AlicizaX
if (_asset == null || _serializedObject == null) if (_asset == null || _serializedObject == null)
{ {
_entryIndices.Clear(); _entryIndices.Clear();
_listView.itemsSource = _entryIndices; _entryList = null;
_listView.Rebuild(); _entryListContainer?.MarkDirtyRepaint();
_emptyContainer.Clear(); _emptyContainer.Clear();
_emptyContainer.Add(new HelpBox("Select or double-click a PoolConfig asset to edit it in this window.", HelpBoxMessageType.Info)); _emptyContainer.Add(new HelpBox("请选择或双击 PoolConfig 资源,然后在这个窗口里编辑对象池规则。", HelpBoxMessageType.Info));
_emptyContainer.style.display = DisplayStyle.Flex; _emptyContainer.style.display = DisplayStyle.Flex;
_detailScrollView.style.display = DisplayStyle.None; _detailScrollView.style.display = DisplayStyle.None;
UpdateRemoveButtonState();
return; return;
} }
RebuildEntryIndices(); RebuildEntryIndices();
ClampSelection(); ClampSelection();
EnsureEntryReorderableList();
SyncEntryListSelection();
_entryListContainer?.MarkDirtyRepaint();
_listView.itemsSource = _entryIndices;
_listView.Rebuild();
if (_entryIndices.Count > 0) if (_entryIndices.Count > 0)
{ {
_listView.SetSelectionWithoutNotify(new[] { _selectedIndex });
_emptyContainer.style.display = DisplayStyle.None; _emptyContainer.style.display = DisplayStyle.None;
_detailScrollView.style.display = DisplayStyle.Flex; _detailScrollView.style.display = DisplayStyle.Flex;
RebuildDetailFields(); RebuildDetailFields();
@ -349,12 +279,10 @@ namespace AlicizaX
else else
{ {
_emptyContainer.Clear(); _emptyContainer.Clear();
_emptyContainer.Add(new HelpBox("No entry selected.", HelpBoxMessageType.Info)); _emptyContainer.Add(new HelpBox("当前没有可编辑的规则,请先新增一条对象池规则。", HelpBoxMessageType.Info));
_emptyContainer.style.display = DisplayStyle.Flex; _emptyContainer.style.display = DisplayStyle.Flex;
_detailScrollView.style.display = DisplayStyle.None; _detailScrollView.style.display = DisplayStyle.None;
} }
UpdateRemoveButtonState();
} }
private void RebuildEntryIndices() private void RebuildEntryIndices()
@ -367,24 +295,121 @@ namespace AlicizaX
} }
} }
private void UpdateRemoveButtonState() private void DrawEntryList()
{ {
Button removeButton = _leftPane?.Q<Button>("remove-button"); if (_asset == null || _serializedObject == null || _entriesProperty == null)
removeButton?.SetEnabled(_entryIndices.Count > 0);
}
private void OnListSelectionChanged()
{
if (_listView.selectedIndex < 0 || _listView.selectedIndex >= _entryIndices.Count)
{ {
return; return;
} }
_selectedIndex = _listView.selectedIndex; EnsureEntryReorderableList();
UpdateRemoveButtonState(); SyncEntryListSelection();
_serializedObject.Update();
_entryListScrollPosition = EditorGUILayout.BeginScrollView(_entryListScrollPosition, GUILayout.ExpandHeight(true), GUILayout.ExpandWidth(true));
Rect listRect = GUILayoutUtility.GetRect(0f, _entryList.GetHeight(), GUILayout.ExpandWidth(true));
_entryList.DoList(listRect);
EditorGUILayout.EndScrollView();
}
private void EnsureEntryReorderableList()
{
if (_serializedObject == null || _entriesProperty == null)
{
_entryList = null;
return;
}
if (_entryList != null && _entryList.serializedProperty == _entriesProperty)
{
return;
}
_entryList = new ReorderableList(_serializedObject, _entriesProperty, true, true, true, true)
{
elementHeight = ListItemHeight + 6f
};
_entryList.drawHeaderCallback = rect =>
{
EditorGUI.LabelField(rect, Utility.Text.Format("对象池规则 ({0})", _entriesProperty.arraySize), EditorStyles.boldLabel);
};
_entryList.drawElementCallback = DrawEntryListElement;
_entryList.onSelectCallback = OnEntryListSelected;
_entryList.onAddCallback = OnEntryListAdd;
_entryList.onRemoveCallback = OnEntryListRemove;
_entryList.onReorderCallback = OnEntryListReordered;
}
private void DrawEntryListElement(Rect rect, int index, bool isActive, bool isFocused)
{
SerializedProperty entry = GetEntryAt(index);
if (entry == null)
{
return;
}
rect.y += 5f;
string primaryLabel = GetPrimaryLabel(entry);
string assetPath = entry.FindPropertyRelative("assetPath").stringValue;
string tooltip = string.IsNullOrWhiteSpace(assetPath) || string.Equals(primaryLabel, assetPath, System.StringComparison.Ordinal)
? primaryLabel
: assetPath;
Rect primaryRect = new Rect(rect.x, rect.y, rect.width, EditorGUIUtility.singleLineHeight);
EditorGUI.LabelField(primaryRect, new GUIContent(primaryLabel, tooltip), EditorStyles.boldLabel);
}
private void OnEntryListSelected(ReorderableList list)
{
if (list.index < 0 || list.index >= _entryIndices.Count)
{
return;
}
_selectedIndex = list.index;
RebuildDetailFields(); RebuildDetailFields();
} }
private void OnEntryListAdd(ReorderableList list)
{
AddEntry();
}
private void OnEntryListRemove(ReorderableList list)
{
RemoveEntry();
}
private void OnEntryListReordered(ReorderableList list)
{
if (_asset == null || _serializedObject == null || _entriesProperty == null)
{
return;
}
RefreshEntryPriorities();
_serializedObject.ApplyModifiedPropertiesWithoutUndo();
_asset.Normalize();
RebuildEntryIndices();
_selectedIndex = Mathf.Clamp(list.index, 0, _entryIndices.Count - 1);
_hasUnsavedChanges = true;
RefreshTitle();
RebuildDetailFields();
_entryListContainer?.MarkDirtyRepaint();
}
private void SyncEntryListSelection()
{
if (_entryList == null)
{
return;
}
_entryList.index = _entryIndices.Count == 0 ? -1 : _selectedIndex;
}
private void RebuildDetailFields() private void RebuildDetailFields()
{ {
if (_asset == null || _serializedObject == null) if (_asset == null || _serializedObject == null)
@ -409,16 +434,124 @@ namespace AlicizaX
bool enterChildren = true; bool enterChildren = true;
while (iterator.NextVisible(enterChildren) && !SerializedProperty.EqualContents(iterator, end)) while (iterator.NextVisible(enterChildren) && !SerializedProperty.EqualContents(iterator, end))
{ {
PropertyField field = new PropertyField(iterator.Copy()); SerializedProperty currentProperty = iterator.Copy();
field.RegisterCallback<SerializedPropertyChangeEvent>(OnDetailPropertyChanged); if (!ShouldDisplayField(currentProperty.name))
_detailFieldsContainer.Add(field); {
enterChildren = false;
continue;
}
_detailFieldsContainer.Add(CreateDetailField(currentProperty));
enterChildren = false; enterChildren = false;
} }
_detailFieldsContainer.Bind(_serializedObject); _detailFieldsContainer.Bind(_serializedObject);
} }
private VisualElement CreateDetailField(SerializedProperty property)
{
if (TryCreateLocalizedEnumField(property, out VisualElement enumFieldContainer))
{
return enumFieldContainer;
}
VisualElement container = new VisualElement();
container.style.flexDirection = FlexDirection.Column;
container.style.marginBottom = 4f;
string label = property.name == "group" ? "\u5206\u7ec4" : GetFieldLabel(property.name);
PropertyField field = new PropertyField(property, label);
if (IsReadOnlyField(property.name))
{
field.SetEnabled(false);
}
field.RegisterCallback<SerializedPropertyChangeEvent>(OnDetailPropertyChanged);
container.Add(field);
string description = property.name == "group"
? "\u7528\u4e8e GameObjectPoolManager \u4e0b\u7684\u7a7a\u95f2\u8282\u70b9\u5f52\u7c7b\uff0c\u4e0d\u586b\u6216\u7a7a\u503c\u4f1a\u81ea\u52a8\u56de\u843d\u5230 DefaultGroup\u3002"
: GetFieldDescription(property.name);
if (!string.IsNullOrWhiteSpace(description))
{
Label descriptionLabel = new Label(description);
descriptionLabel.style.whiteSpace = WhiteSpace.Normal;
descriptionLabel.style.fontSize = 11f;
descriptionLabel.style.color = DescriptionColor;
descriptionLabel.style.marginLeft = 4f;
descriptionLabel.style.marginRight = 4f;
descriptionLabel.style.marginTop = -2f;
descriptionLabel.style.marginBottom = 6f;
container.Add(descriptionLabel);
}
return container;
}
private bool TryCreateLocalizedEnumField(SerializedProperty property, out VisualElement container)
{
List<string> options = GetEnumOptions(property.name);
if (options == null)
{
container = null;
return false;
}
container = new VisualElement();
container.style.flexDirection = FlexDirection.Column;
container.style.marginBottom = 4f;
int currentIndex = Mathf.Clamp(property.enumValueIndex, 0, options.Count - 1);
PopupField<string> popupField = new PopupField<string>(GetFieldLabel(property.name), options, currentIndex);
string propertyPath = property.propertyPath;
popupField.RegisterValueChangedCallback(_ =>
{
if (_serializedObject == null)
{
return;
}
int selectedIndex = options.IndexOf(popupField.value);
if (selectedIndex < 0)
{
return;
}
_serializedObject.Update();
SerializedProperty targetProperty = _serializedObject.FindProperty(propertyPath);
if (targetProperty == null || targetProperty.enumValueIndex == selectedIndex)
{
return;
}
targetProperty.enumValueIndex = selectedIndex;
ApplyDetailPropertyChanges();
});
container.Add(popupField);
string description = GetFieldDescription(property.name);
if (!string.IsNullOrWhiteSpace(description))
{
Label descriptionLabel = new Label(description);
descriptionLabel.style.whiteSpace = WhiteSpace.Normal;
descriptionLabel.style.fontSize = 11f;
descriptionLabel.style.color = DescriptionColor;
descriptionLabel.style.marginLeft = 4f;
descriptionLabel.style.marginRight = 4f;
descriptionLabel.style.marginTop = -2f;
descriptionLabel.style.marginBottom = 6f;
container.Add(descriptionLabel);
}
return true;
}
private void OnDetailPropertyChanged(SerializedPropertyChangeEvent evt) private void OnDetailPropertyChanged(SerializedPropertyChangeEvent evt)
{
ApplyDetailPropertyChanges();
}
private void ApplyDetailPropertyChanges()
{ {
if (_asset == null || _serializedObject == null) if (_asset == null || _serializedObject == null)
{ {
@ -428,7 +561,7 @@ namespace AlicizaX
_serializedObject.ApplyModifiedPropertiesWithoutUndo(); _serializedObject.ApplyModifiedPropertiesWithoutUndo();
_asset.Normalize(); _asset.Normalize();
_hasUnsavedChanges = true; _hasUnsavedChanges = true;
_listView?.RefreshItems(); _entryListContainer?.MarkDirtyRepaint();
RefreshTitle(); RefreshTitle();
_detailTitleLabel.text = GetPrimaryLabel(GetSelectedProperty()); _detailTitleLabel.text = GetPrimaryLabel(GetSelectedProperty());
} }
@ -446,6 +579,7 @@ namespace AlicizaX
_entriesProperty.InsertArrayElementAtIndex(index); _entriesProperty.InsertArrayElementAtIndex(index);
SerializedProperty property = _entriesProperty.GetArrayElementAtIndex(index); SerializedProperty property = _entriesProperty.GetArrayElementAtIndex(index);
InitializeNewEntry(property, index); InitializeNewEntry(property, index);
RefreshEntryPriorities();
_serializedObject.ApplyModifiedPropertiesWithoutUndo(); _serializedObject.ApplyModifiedPropertiesWithoutUndo();
_asset.Normalize(); _asset.Normalize();
@ -453,7 +587,6 @@ namespace AlicizaX
_selectedIndex = index; _selectedIndex = index;
_hasUnsavedChanges = true; _hasUnsavedChanges = true;
RefreshUi(); RefreshUi();
_listView.SetSelectionWithoutNotify(new[] { _selectedIndex });
} }
private void RemoveEntry() private void RemoveEntry()
@ -465,6 +598,7 @@ namespace AlicizaX
_serializedObject.Update(); _serializedObject.Update();
_entriesProperty.DeleteArrayElementAtIndex(_selectedIndex); _entriesProperty.DeleteArrayElementAtIndex(_selectedIndex);
RefreshEntryPriorities();
_serializedObject.ApplyModifiedPropertiesWithoutUndo(); _serializedObject.ApplyModifiedPropertiesWithoutUndo();
_asset.Normalize(); _asset.Normalize();
RebuildEntryIndices(); RebuildEntryIndices();
@ -476,38 +610,47 @@ namespace AlicizaX
_hasUnsavedChanges = true; _hasUnsavedChanges = true;
RefreshUi(); RefreshUi();
if (_entryIndices.Count > 0)
{
_listView.SetSelectionWithoutNotify(new[] { _selectedIndex });
}
} }
private void InitializeNewEntry(SerializedProperty property, int index) private void InitializeNewEntry(SerializedProperty property, int index)
{ {
property.FindPropertyRelative("entryName").stringValue = $"PoolEntry{index + 1}"; property.FindPropertyRelative("entryName").stringValue = Utility.Text.Format("对象池规则{0}", index + 1);
property.FindPropertyRelative("group").stringValue = PoolEntry.DefaultGroup; property.FindPropertyRelative("group").stringValue = PoolEntry.DefaultGroup;
property.FindPropertyRelative("assetPath").stringValue = string.Empty; property.FindPropertyRelative("assetPath").stringValue = string.Empty;
property.FindPropertyRelative("matchMode").enumValueIndex = (int)PoolMatchMode.Exact; property.FindPropertyRelative("matchMode").enumValueIndex = (int)PoolMatchMode.Exact;
property.FindPropertyRelative("loaderType").enumValueIndex = (int)PoolResourceLoaderType.AssetBundle; property.FindPropertyRelative("loaderType").enumValueIndex = (int)PoolResourceLoaderType.AssetBundle;
property.FindPropertyRelative("overflowPolicy").enumValueIndex = (int)PoolOverflowPolicy.FailFast; property.FindPropertyRelative("category").enumValueIndex = (int)PoolCategory.Default;
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("softCapacity").intValue = 8;
property.FindPropertyRelative("hardCapacity").intValue = 8; property.FindPropertyRelative("hardCapacity").intValue = 16;
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; property.FindPropertyRelative("priority").intValue = index;
} }
private void RefreshEntryPriorities()
{
if (_entriesProperty == null)
{
return;
}
int count = _entriesProperty.arraySize;
for (int i = 0; i < count; i++)
{
SerializedProperty entry = _entriesProperty.GetArrayElementAtIndex(i);
entry.FindPropertyRelative("priority").intValue = count - i;
}
}
private static string GetAssetGuid(PoolConfigScriptableObject asset)
{
if (asset == null)
{
return string.Empty;
}
string assetPath = AssetDatabase.GetAssetPath(asset);
return string.IsNullOrEmpty(assetPath) ? string.Empty : AssetDatabase.AssetPathToGUID(assetPath);
}
private void SaveAsset() private void SaveAsset()
{ {
if (_asset == null || _serializedObject == null) if (_asset == null || _serializedObject == null)
@ -526,10 +669,10 @@ namespace AlicizaX
private void RefreshTitle() private void RefreshTitle()
{ {
string assetLabel = _asset == null ? "No PoolConfig Selected" : _asset.name; string assetLabel = _asset == null ? "未选择 PoolConfig" : _asset.name;
if (_hasUnsavedChanges) if (_hasUnsavedChanges)
{ {
assetLabel += " *"; assetLabel = Utility.Text.Format("{0} *", assetLabel);
} }
_titleLabel.text = assetLabel; _titleLabel.text = assetLabel;
@ -557,7 +700,7 @@ namespace AlicizaX
{ {
if (property == null) if (property == null)
{ {
return "<Missing>"; return "<规则缺失>";
} }
string entryName = property.FindPropertyRelative("entryName").stringValue; string entryName = property.FindPropertyRelative("entryName").stringValue;
@ -567,19 +710,59 @@ namespace AlicizaX
return entryName; return entryName;
} }
return string.IsNullOrWhiteSpace(assetPath) ? "<Unnamed Entry>" : assetPath; return string.IsNullOrWhiteSpace(assetPath) ? "<未命名规则>" : assetPath;
} }
private string GetSecondaryLabel(SerializedProperty property) private static List<string> GetEnumOptions(string propertyName)
{ {
if (property == null) return propertyName switch
{ {
return string.Empty; "matchMode" => MatchModeOptions,
} "loaderType" => LoaderTypeOptions,
_ => null
};
}
string group = property.FindPropertyRelative("group").stringValue; private static bool ShouldDisplayField(string propertyName)
string assetPath = property.FindPropertyRelative("assetPath").stringValue; {
return string.IsNullOrWhiteSpace(assetPath) ? group : $"{group} | {assetPath}"; return propertyName != "category";
}
private static bool IsReadOnlyField(string propertyName)
{
return propertyName == "priority";
}
private static string GetFieldLabel(string propertyName)
{
return propertyName switch
{
"entryName" => "规则名称",
"group" => "分组",
"assetPath" => "资源路径",
"matchMode" => "匹配模式",
"loaderType" => "加载器类型",
"softCapacity" => "软容量",
"hardCapacity" => "容量",
"priority" => "优先级",
_ => propertyName
};
}
private static string GetFieldDescription(string propertyName)
{
return propertyName switch
{
"entryName" => "规则名称就是主定位信息。列表、调试和问题排查都直接看这个名字。",
"group" => "用于 GameObjectPoolManager 下的空闲节点归类。不填或空值会自动回落到 DefaultGroup。",
"assetPath" => "要匹配的资源路径。精确匹配填完整路径,前缀匹配可填写目录前缀。",
"matchMode" => "精确匹配只命中单一路径,前缀匹配适合同目录或同类资源共用规则。",
"loaderType" => "决定 Prefab 从哪个资源通道加载。AssetBundle 走包体资源Resources 走内置目录。",
"softCapacity" => "超过该值后,维护阶段会优先回收空闲实例。",
"hardCapacity" => "基础容量。超过这个值会自动扩容并输出警告,后续维护回收会再收回到这个基准。",
"priority" => "由左侧拖拽顺序自动维护,越靠上优先级越高。",
_ => string.Empty
};
} }
private void ClampSelection() private void ClampSelection()

View File

@ -1,5 +1,4 @@
using UnityEditor; using UnityEditor;
using UnityEngine;
namespace AlicizaX namespace AlicizaX
{ {
@ -8,10 +7,7 @@ namespace AlicizaX
{ {
public override void OnInspectorGUI() public override void OnInspectorGUI()
{ {
if (GUILayout.Button("Open Editor", GUILayout.Width(160f), GUILayout.Height(28f))) DrawDefaultInspector();
{
PoolConfigEditorWindow.Open((PoolConfigScriptableObject)target);
}
} }
} }
} }

View File

@ -1,22 +1,7 @@
using System;
using System.Collections.Generic;
using UnityEngine; using UnityEngine;
namespace AlicizaX 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] [DisallowMultipleComponent]
public sealed class PoolAutoRecycleParticleStop : MonoBehaviour, IGameObjectPoolable public sealed class PoolAutoRecycleParticleStop : MonoBehaviour, IGameObjectPoolable
{ {
@ -76,168 +61,18 @@ namespace AlicizaX
} }
} }
[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] [DisallowMultipleComponent]
public sealed class PoolSleepableGameObjectGroup : MonoBehaviour, IPoolSleepable public sealed class PoolSleepableGameObjectGroup : MonoBehaviour, IPoolSleepable
{ {
[SerializeField] private List<Behaviour> disableOnSleep = new List<Behaviour>(); [SerializeField] private Behaviour[] disableOnSleep;
[SerializeField] private List<Renderer> hideOnSleep = new List<Renderer>(); [SerializeField] private Renderer[] hideOnSleep;
[SerializeField] private List<Collider> disableCollider3D = new List<Collider>(); [SerializeField] private Collider[] disableCollider3D;
[SerializeField] private List<Collider2D> disableCollider2D = new List<Collider2D>(); [SerializeField] private Collider2D[] disableCollider2D;
public void EnterSleep() public void EnterSleep()
{ {
for (int i = 0; i < disableOnSleep.Count; i++) int disableCount = disableOnSleep == null ? 0 : disableOnSleep.Length;
for (int i = 0; i < disableCount; i++)
{ {
if (disableOnSleep[i] != null) if (disableOnSleep[i] != null)
{ {
@ -245,7 +80,8 @@ namespace AlicizaX
} }
} }
for (int i = 0; i < hideOnSleep.Count; i++) int hideCount = hideOnSleep == null ? 0 : hideOnSleep.Length;
for (int i = 0; i < hideCount; i++)
{ {
if (hideOnSleep[i] != null) if (hideOnSleep[i] != null)
{ {
@ -253,7 +89,8 @@ namespace AlicizaX
} }
} }
for (int i = 0; i < disableCollider3D.Count; i++) int collider3DCount = disableCollider3D == null ? 0 : disableCollider3D.Length;
for (int i = 0; i < collider3DCount; i++)
{ {
if (disableCollider3D[i] != null) if (disableCollider3D[i] != null)
{ {
@ -261,7 +98,8 @@ namespace AlicizaX
} }
} }
for (int i = 0; i < disableCollider2D.Count; i++) int collider2DCount = disableCollider2D == null ? 0 : disableCollider2D.Length;
for (int i = 0; i < collider2DCount; i++)
{ {
if (disableCollider2D[i] != null) if (disableCollider2D[i] != null)
{ {
@ -272,7 +110,8 @@ namespace AlicizaX
public void ExitSleep(in PoolSpawnContext context) public void ExitSleep(in PoolSpawnContext context)
{ {
for (int i = 0; i < disableOnSleep.Count; i++) int disableCount = disableOnSleep == null ? 0 : disableOnSleep.Length;
for (int i = 0; i < disableCount; i++)
{ {
if (disableOnSleep[i] != null) if (disableOnSleep[i] != null)
{ {
@ -280,7 +119,8 @@ namespace AlicizaX
} }
} }
for (int i = 0; i < hideOnSleep.Count; i++) int hideCount = hideOnSleep == null ? 0 : hideOnSleep.Length;
for (int i = 0; i < hideCount; i++)
{ {
if (hideOnSleep[i] != null) if (hideOnSleep[i] != null)
{ {
@ -288,7 +128,8 @@ namespace AlicizaX
} }
} }
for (int i = 0; i < disableCollider3D.Count; i++) int collider3DCount = disableCollider3D == null ? 0 : disableCollider3D.Length;
for (int i = 0; i < collider3DCount; i++)
{ {
if (disableCollider3D[i] != null) if (disableCollider3D[i] != null)
{ {
@ -296,7 +137,8 @@ namespace AlicizaX
} }
} }
for (int i = 0; i < disableCollider2D.Count; i++) int collider2DCount = disableCollider2D == null ? 0 : disableCollider2D.Length;
for (int i = 0; i < collider2DCount; i++)
{ {
if (disableCollider2D[i] != null) if (disableCollider2D[i] != null)
{ {

View File

@ -1,5 +1,5 @@
using System; using System;
using System.Collections.Generic; using AlicizaX.ObjectPool;
using UnityEngine; using UnityEngine;
namespace AlicizaX namespace AlicizaX
@ -16,84 +16,34 @@ namespace AlicizaX
Prefix = 1 Prefix = 1
} }
public enum PoolOverflowPolicy public enum PoolCategory
{ {
FailFast = 0, Default = 0,
InstantiateOneShot = 1, Effect = 1,
AutoExpand = 2, Monster = 2,
RecycleOldestInactive = 3, Building = 3,
DropNewestRequest = 4 UI = 4,
} Custom = 5
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 PoolEntry public sealed class PoolEntry
{ {
public const string DefaultGroup = "Default"; public const string DefaultGroup = "DefaultGroup";
public const string DefaultEntryName = "DefaultPool"; public const string DefaultEntryName = "PoolRule";
public string entryName = DefaultEntryName; public string entryName = DefaultEntryName;
public string group = DefaultGroup; public string group = DefaultGroup;
public string assetPath; public string assetPath = string.Empty;
public PoolMatchMode matchMode = PoolMatchMode.Exact; public PoolMatchMode matchMode = PoolMatchMode.Exact;
public PoolResourceLoaderType loaderType = PoolResourceLoaderType.AssetBundle; public PoolResourceLoaderType loaderType = PoolResourceLoaderType.AssetBundle;
public PoolOverflowPolicy overflowPolicy = PoolOverflowPolicy.FailFast; public PoolCategory category = PoolCategory.Default;
public PoolTrimPolicy trimPolicy = PoolTrimPolicy.IdleOnly;
public PoolActivationMode activationMode = PoolActivationMode.SetActive;
public PoolResetMode resetMode = PoolResetMode.PoolableCallbacks;
[Min(0)]
public int minRetained = 0;
[Min(1)] [Min(1)]
public int softCapacity = 8; public int softCapacity = 8;
[Min(1)] [Min(1)]
public int hardCapacity = 8; public int hardCapacity = 16;
[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 int priority;
public void Normalize() public void Normalize()
@ -101,43 +51,26 @@ namespace AlicizaX
entryName = string.IsNullOrWhiteSpace(entryName) ? DefaultEntryName : entryName.Trim(); 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);
minRetained = Mathf.Max(0, minRetained);
softCapacity = Mathf.Max(1, softCapacity); softCapacity = Mathf.Max(1, softCapacity);
hardCapacity = Mathf.Max(softCapacity, hardCapacity); hardCapacity = Mathf.Max(softCapacity, hardCapacity);
minRetained = Mathf.Min(minRetained, hardCapacity);
idleTrimDelay = Mathf.Max(0f, idleTrimDelay);
prefabUnloadDelay = Mathf.Max(0f, prefabUnloadDelay);
autoRecycleDelay = Mathf.Max(0f, autoRecycleDelay);
trimBatchPerTick = Mathf.Max(1, trimBatchPerTick);
warmupBatchPerFrame = Mathf.Max(1, warmupBatchPerFrame);
warmupFrameBudgetMs = Mathf.Max(0f, warmupFrameBudgetMs);
} }
public bool Matches(string requestedAssetPath, string requestedGroup = null) public bool Matches(string requestedAssetPath, string requestedGroup = null)
{ {
if (string.IsNullOrWhiteSpace(assetPath) || string.IsNullOrWhiteSpace(requestedAssetPath)) if (string.IsNullOrEmpty(assetPath) || string.IsNullOrEmpty(requestedAssetPath))
{ {
return false; return false;
} }
if (!string.IsNullOrWhiteSpace(requestedGroup) && if (!string.IsNullOrEmpty(requestedGroup) &&
!string.Equals(group, requestedGroup, StringComparison.Ordinal)) !string.Equals(group, requestedGroup, StringComparison.Ordinal))
{ {
return false; return false;
} }
return matchMode switch return matchMode == PoolMatchMode.Exact
{ ? string.Equals(requestedAssetPath, assetPath, StringComparison.Ordinal)
PoolMatchMode.Exact => string.Equals(requestedAssetPath, assetPath, StringComparison.Ordinal), : requestedAssetPath.StartsWith(assetPath, StringComparison.Ordinal);
PoolMatchMode.Prefix => requestedAssetPath.StartsWith(assetPath, StringComparison.Ordinal),
_ => false
};
}
public string BuildResolvedPoolKey(string resolvedAssetPath)
{
string concretePath = NormalizeAssetPath(resolvedAssetPath);
return $"{group}|{(int)matchMode}|{assetPath}|{(int)loaderType}|{concretePath}|{entryName}";
} }
public static int CompareByPriority(PoolEntry left, PoolEntry right) public static int CompareByPriority(PoolEntry left, PoolEntry right)
@ -169,8 +102,8 @@ namespace AlicizaX
return modeCompare; return modeCompare;
} }
int leftLength = left.assetPath?.Length ?? 0; int leftLength = left.assetPath == null ? 0 : left.assetPath.Length;
int rightLength = right.assetPath?.Length ?? 0; int rightLength = right.assetPath == null ? 0 : right.assetPath.Length;
int pathLengthCompare = rightLength.CompareTo(leftLength); int pathLengthCompare = rightLength.CompareTo(leftLength);
if (pathLengthCompare != 0) if (pathLengthCompare != 0)
{ {
@ -191,81 +124,344 @@ namespace AlicizaX
} }
} }
[Serializable] internal struct PoolCompiledRule
public sealed class ResolvedPoolConfig
{ {
public int ruleIndex;
public string entryName; public string entryName;
public string group; public string group;
public string assetPath; public string assetPath;
public PoolMatchMode matchMode; public PoolMatchMode matchMode;
public PoolResourceLoaderType loaderType; public PoolResourceLoaderType loaderType;
public PoolOverflowPolicy overflowPolicy; public PoolCategory category;
public PoolTrimPolicy trimPolicy;
public PoolActivationMode activationMode;
public PoolResetMode resetMode;
public int minRetained;
public int softCapacity; public int softCapacity;
public int hardCapacity; 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 int priority;
public string BuildResolvedPoolKey(string resolvedAssetPath) public bool IsPrefix => matchMode == PoolMatchMode.Prefix;
{
string concretePath = PoolEntry.NormalizeAssetPath(resolvedAssetPath);
return $"{group}|{(int)matchMode}|{assetPath}|{(int)loaderType}|{concretePath}|{entryName}";
}
public static ResolvedPoolConfig From(PoolEntry entry) public static PoolCompiledRule FromEntry(PoolEntry entry, int ruleIndex)
{ {
if (entry == null) return new PoolCompiledRule
{
throw new ArgumentNullException(nameof(entry));
}
return new ResolvedPoolConfig
{ {
ruleIndex = ruleIndex,
entryName = entry.entryName, entryName = entry.entryName,
group = entry.group, group = entry.group,
assetPath = entry.assetPath, assetPath = entry.assetPath,
matchMode = entry.matchMode, matchMode = entry.matchMode,
loaderType = entry.loaderType, loaderType = entry.loaderType,
overflowPolicy = entry.overflowPolicy, category = entry.category,
trimPolicy = entry.trimPolicy,
activationMode = entry.activationMode,
resetMode = entry.resetMode,
minRetained = entry.minRetained,
softCapacity = entry.softCapacity, softCapacity = entry.softCapacity,
hardCapacity = entry.hardCapacity, 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 priority = entry.priority
}; };
} }
} }
[Serializable] internal sealed class PoolCompiledCatalog
public sealed class PoolConfigCatalog
{ {
public readonly List<PoolEntry> entries; private readonly PoolCompiledRule[] _rules;
private StringOpenHashMap _groupIndexMap;
private PoolCompiledGroup[] _groups;
private StringOpenHashMap _globalExactMap;
private PoolPrefixTrie _globalPrefixTrie;
public PoolConfigCatalog(List<PoolEntry> entries) private PoolCompiledCatalog(
PoolCompiledRule[] rules,
StringOpenHashMap groupIndexMap,
PoolCompiledGroup[] groups,
StringOpenHashMap globalExactMap,
PoolPrefixTrie globalPrefixTrie)
{ {
this.entries = entries ?? throw new ArgumentNullException(nameof(entries)); _rules = rules;
_groupIndexMap = groupIndexMap;
_groups = groups;
_globalExactMap = globalExactMap;
_globalPrefixTrie = globalPrefixTrie;
}
public bool IsEmpty => _rules == null || _rules.Length == 0;
public int RuleCount => _rules == null ? 0 : _rules.Length;
public ref readonly PoolCompiledRule GetRule(int ruleIndex)
{
return ref _rules[ruleIndex];
}
public int Resolve(string assetPath, string group)
{
if (string.IsNullOrEmpty(assetPath) || _rules == null || _rules.Length == 0)
{
return -1;
}
if (!string.IsNullOrEmpty(group))
{
if (_groupIndexMap.TryGetValue(group, out int groupIndex))
{
return _groups[groupIndex].Resolve(assetPath);
}
return -1;
}
if (_globalExactMap.TryGetValue(assetPath, out int exactRuleIndex))
{
return exactRuleIndex;
}
return _globalPrefixTrie.Resolve(assetPath);
}
public static PoolCompiledCatalog Empty()
{
return new PoolCompiledCatalog(
Array.Empty<PoolCompiledRule>(),
new StringOpenHashMap(8),
Array.Empty<PoolCompiledGroup>(),
new StringOpenHashMap(8),
new PoolPrefixTrie(0));
}
public static PoolCompiledCatalog Build(PoolEntry[] entries)
{
if (entries == null || entries.Length == 0)
{
return Empty();
}
int entryCount = entries.Length;
var groupIndexMap = new StringOpenHashMap(entryCount);
var groupNames = new string[entryCount];
var groupPrefixChars = new int[entryCount];
int groupCount = 0;
int globalPrefixChars = 0;
for (int i = 0; i < entryCount; i++)
{
PoolEntry entry = entries[i];
if (entry == null || string.IsNullOrEmpty(entry.assetPath))
{
continue;
}
if (!groupIndexMap.TryGetValue(entry.group, out int groupIndex))
{
groupIndex = groupCount;
groupIndexMap.AddOrUpdate(entry.group, groupIndex);
groupNames[groupCount] = entry.group;
groupCount++;
}
if (entry.matchMode == PoolMatchMode.Prefix)
{
int pathLength = entry.assetPath.Length;
groupPrefixChars[groupIndex] += pathLength;
globalPrefixChars += pathLength;
}
}
var groups = new PoolCompiledGroup[groupCount];
for (int i = 0; i < groupCount; i++)
{
groups[i] = new PoolCompiledGroup(groupNames[i], entryCount, groupPrefixChars[i]);
}
var rules = new PoolCompiledRule[entryCount];
var globalExactMap = new StringOpenHashMap(entryCount);
var globalPrefixTrie = new PoolPrefixTrie(globalPrefixChars);
for (int i = 0; i < entryCount; i++)
{
PoolEntry entry = entries[i];
if (entry == null || string.IsNullOrEmpty(entry.assetPath))
{
continue;
}
PoolCompiledRule rule = PoolCompiledRule.FromEntry(entry, i);
rules[i] = rule;
groupIndexMap.TryGetValue(rule.group, out int groupIndex);
groups[groupIndex].Register(rule);
if (rule.matchMode == PoolMatchMode.Exact)
{
if (!globalExactMap.TryGetValue(rule.assetPath, out _))
{
globalExactMap.AddOrUpdate(rule.assetPath, i);
}
}
else
{
globalPrefixTrie.Register(rule.assetPath, i);
}
}
return new PoolCompiledCatalog(rules, groupIndexMap, groups, globalExactMap, globalPrefixTrie);
}
}
internal sealed class PoolCompiledGroup
{
private readonly string _name;
private StringOpenHashMap _exactMap;
private PoolPrefixTrie _prefixTrie;
public PoolCompiledGroup(string name, int exactCapacity, int prefixChars)
{
_name = name;
_exactMap = new StringOpenHashMap(exactCapacity);
_prefixTrie = new PoolPrefixTrie(prefixChars);
}
public void Register(in PoolCompiledRule rule)
{
if (rule.matchMode == PoolMatchMode.Exact)
{
if (!_exactMap.TryGetValue(rule.assetPath, out _))
{
_exactMap.AddOrUpdate(rule.assetPath, rule.ruleIndex);
}
return;
}
_prefixTrie.Register(rule.assetPath, rule.ruleIndex);
}
public int Resolve(string assetPath)
{
if (_exactMap.TryGetValue(assetPath, out int exactRuleIndex))
{
return exactRuleIndex;
}
return _prefixTrie.Resolve(assetPath);
}
}
internal sealed class PoolPrefixTrie
{
private struct Node
{
public char character;
public int firstChild;
public int nextSibling;
public int ruleIndex;
}
private Node[] _nodes;
private int _nodeCount;
public PoolPrefixTrie(int prefixCharCount)
{
int capacity = Mathf.Max(1, prefixCharCount + 1);
_nodes = new Node[capacity];
_nodes[0].firstChild = -1;
_nodes[0].nextSibling = -1;
_nodes[0].ruleIndex = -1;
_nodeCount = 1;
}
public void Register(string prefix, int ruleIndex)
{
if (string.IsNullOrEmpty(prefix))
{
return;
}
int nodeIndex = 0;
int length = prefix.Length;
for (int i = 0; i < length; i++)
{
nodeIndex = GetOrCreateChild(nodeIndex, prefix[i]);
}
if (_nodes[nodeIndex].ruleIndex < 0)
{
_nodes[nodeIndex].ruleIndex = ruleIndex;
}
}
public int Resolve(string value)
{
if (string.IsNullOrEmpty(value) || _nodes == null)
{
return -1;
}
int nodeIndex = 0;
int bestRuleIndex = -1;
int length = value.Length;
for (int i = 0; i < length; i++)
{
nodeIndex = FindChild(nodeIndex, value[i]);
if (nodeIndex < 0)
{
break;
}
int matchedRuleIndex = _nodes[nodeIndex].ruleIndex;
if (matchedRuleIndex >= 0)
{
bestRuleIndex = matchedRuleIndex;
}
}
return bestRuleIndex;
}
private int GetOrCreateChild(int nodeIndex, char character)
{
int childIndex = _nodes[nodeIndex].firstChild;
while (childIndex >= 0)
{
if (_nodes[childIndex].character == character)
{
return childIndex;
}
childIndex = _nodes[childIndex].nextSibling;
}
EnsureCapacity(_nodeCount + 1);
int newNodeIndex = _nodeCount++;
_nodes[newNodeIndex].character = character;
_nodes[newNodeIndex].firstChild = -1;
_nodes[newNodeIndex].nextSibling = _nodes[nodeIndex].firstChild;
_nodes[newNodeIndex].ruleIndex = -1;
_nodes[nodeIndex].firstChild = newNodeIndex;
return newNodeIndex;
}
private int FindChild(int nodeIndex, char character)
{
int childIndex = _nodes[nodeIndex].firstChild;
while (childIndex >= 0)
{
if (_nodes[childIndex].character == character)
{
return childIndex;
}
childIndex = _nodes[childIndex].nextSibling;
}
return -1;
}
private void EnsureCapacity(int required)
{
if (_nodes.Length >= required)
{
return;
}
int newCapacity = Mathf.Max(required, _nodes.Length << 1);
var newNodes = new Node[newCapacity];
Array.Copy(_nodes, 0, newNodes, 0, _nodeCount);
_nodes = newNodes;
} }
} }
} }

View File

@ -1,31 +1,53 @@
using System;
using System.Collections.Generic; using System.Collections.Generic;
using UnityEngine; using UnityEngine;
namespace AlicizaX namespace AlicizaX
{ {
[CreateAssetMenu(fileName = "PoolConfig", menuName = "GameplaySystem/PoolConfig", order = 10)] [CreateAssetMenu(fileName = "PoolConfig", menuName = "GameplaySystem/PoolConfig", order = 10)]
public class PoolConfigScriptableObject : ScriptableObject public sealed class PoolConfigScriptableObject : ScriptableObject
{ {
public List<PoolEntry> entries = new List<PoolEntry>(); public List<PoolEntry> entries = new List<PoolEntry>();
public PoolConfigCatalog BuildCatalog() internal PoolCompiledCatalog BuildCatalog()
{ {
Normalize(); Normalize();
var normalizedEntries = new List<PoolEntry>(entries.Count); if (entries == null || entries.Count == 0)
{
return PoolCompiledCatalog.Empty();
}
int validCount = 0;
for (int i = 0; i < entries.Count; i++) for (int i = 0; i < entries.Count; i++)
{ {
PoolEntry entry = entries[i]; PoolEntry entry = entries[i];
if (entry == null) if (entry != null && !string.IsNullOrEmpty(entry.assetPath))
{
validCount++;
}
}
if (validCount == 0)
{
return PoolCompiledCatalog.Empty();
}
var normalizedEntries = new PoolEntry[validCount];
int writeIndex = 0;
for (int i = 0; i < entries.Count; i++)
{
PoolEntry entry = entries[i];
if (entry == null || string.IsNullOrEmpty(entry.assetPath))
{ {
continue; continue;
} }
normalizedEntries.Add(entry); normalizedEntries[writeIndex++] = entry;
} }
normalizedEntries.Sort(PoolEntry.CompareByPriority); Array.Sort(normalizedEntries, PoolEntry.CompareByPriority);
return new PoolConfigCatalog(normalizedEntries); return PoolCompiledCatalog.Build(normalizedEntries);
} }
public void Normalize() public void Normalize()
@ -33,6 +55,7 @@ namespace AlicizaX
if (entries == null) if (entries == null)
{ {
entries = new List<PoolEntry>(); entries = new List<PoolEntry>();
return;
} }
for (int i = 0; i < entries.Count; i++) for (int i = 0; i < entries.Count; i++)

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff