2026-04-17 21:01:20 +08:00
|
|
|
|
using System.Collections.Generic;
|
|
|
|
|
|
using AlicizaX.Editor;
|
|
|
|
|
|
using UnityEditor;
|
|
|
|
|
|
using UnityEditor.Callbacks;
|
|
|
|
|
|
using UnityEditor.UIElements;
|
2026-04-30 17:18:17 +08:00
|
|
|
|
using UnityEditorInternal;
|
2026-04-17 21:01:20 +08:00
|
|
|
|
using UnityEngine;
|
|
|
|
|
|
using UnityEngine.UIElements;
|
|
|
|
|
|
|
|
|
|
|
|
namespace AlicizaX
|
|
|
|
|
|
{
|
|
|
|
|
|
public sealed class PoolConfigEditorWindow : EditorWindow
|
|
|
|
|
|
{
|
|
|
|
|
|
private const float MinLeftWidth = 240f;
|
|
|
|
|
|
private const float InitialLeftWidth = 300f;
|
2026-04-30 17:18:17 +08:00
|
|
|
|
private const int ListItemHeight = 24;
|
|
|
|
|
|
private const string WindowTitle = "对象池配置编辑器";
|
2026-04-17 21:01:20 +08:00
|
|
|
|
|
|
|
|
|
|
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);
|
2026-04-30 17:18:17 +08:00
|
|
|
|
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" };
|
2026-04-17 21:01:20 +08:00
|
|
|
|
|
2026-04-30 17:18:17 +08:00
|
|
|
|
[SerializeField]
|
2026-04-17 21:01:20 +08:00
|
|
|
|
private PoolConfigScriptableObject _asset;
|
2026-04-30 17:18:17 +08:00
|
|
|
|
[SerializeField]
|
|
|
|
|
|
private string _assetGuid;
|
2026-04-17 21:01:20 +08:00
|
|
|
|
private SerializedObject _serializedObject;
|
|
|
|
|
|
private SerializedProperty _entriesProperty;
|
|
|
|
|
|
private readonly List<int> _entryIndices = new List<int>();
|
|
|
|
|
|
|
2026-04-30 17:18:17 +08:00
|
|
|
|
[SerializeField]
|
2026-04-17 21:01:20 +08:00
|
|
|
|
private int _selectedIndex;
|
2026-04-30 17:18:17 +08:00
|
|
|
|
[SerializeField]
|
2026-04-17 21:01:20 +08:00
|
|
|
|
private bool _hasUnsavedChanges;
|
2026-04-30 17:18:17 +08:00
|
|
|
|
[SerializeField]
|
|
|
|
|
|
private Vector2 _entryListScrollPosition;
|
2026-04-17 21:01:20 +08:00
|
|
|
|
|
|
|
|
|
|
private ToolbarButton _saveButton;
|
|
|
|
|
|
private Label _titleLabel;
|
|
|
|
|
|
private VisualElement _leftPane;
|
2026-04-30 17:18:17 +08:00
|
|
|
|
private IMGUIContainer _entryListContainer;
|
|
|
|
|
|
private ReorderableList _entryList;
|
2026-04-17 21:01:20 +08:00
|
|
|
|
private ScrollView _detailScrollView;
|
|
|
|
|
|
private Label _detailTitleLabel;
|
|
|
|
|
|
private VisualElement _detailFieldsContainer;
|
|
|
|
|
|
private VisualElement _emptyContainer;
|
|
|
|
|
|
|
2026-04-30 17:18:17 +08:00
|
|
|
|
private static void OpenForAsset(PoolConfigScriptableObject asset)
|
2026-04-17 21:01:20 +08:00
|
|
|
|
{
|
2026-04-30 17:18:17 +08:00
|
|
|
|
if (asset == null)
|
|
|
|
|
|
{
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2026-04-17 21:01:20 +08:00
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-30 17:18:17 +08:00
|
|
|
|
OpenForAsset(asset);
|
2026-04-17 21:01:20 +08:00
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private void CreateGUI()
|
|
|
|
|
|
{
|
|
|
|
|
|
titleContent = new GUIContent(WindowTitle, EditorGUIUtility.IconContent("ScriptableObject Icon").image);
|
|
|
|
|
|
BuildUi();
|
2026-04-30 17:18:17 +08:00
|
|
|
|
RestoreWindowState();
|
|
|
|
|
|
RefreshUi();
|
2026-04-17 21:01:20 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private void OnEnable()
|
|
|
|
|
|
{
|
|
|
|
|
|
titleContent = new GUIContent(WindowTitle, EditorGUIUtility.IconContent("ScriptableObject Icon").image);
|
|
|
|
|
|
if (rootVisualElement.childCount == 0)
|
|
|
|
|
|
{
|
|
|
|
|
|
BuildUi();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-30 17:18:17 +08:00
|
|
|
|
RestoreWindowState();
|
|
|
|
|
|
RefreshUi();
|
2026-04-17 21:01:20 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private void BuildUi()
|
|
|
|
|
|
{
|
|
|
|
|
|
rootVisualElement.Clear();
|
|
|
|
|
|
rootVisualElement.style.flexDirection = FlexDirection.Column;
|
|
|
|
|
|
|
|
|
|
|
|
Toolbar toolbar = new Toolbar();
|
|
|
|
|
|
toolbar.style.flexShrink = 0f;
|
|
|
|
|
|
|
|
|
|
|
|
_saveButton = new ToolbarButton(SaveAsset)
|
|
|
|
|
|
{
|
2026-04-30 17:18:17 +08:00
|
|
|
|
tooltip = "保存当前 PoolConfig 配置"
|
2026-04-17 21:01:20 +08:00
|
|
|
|
};
|
|
|
|
|
|
_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()
|
|
|
|
|
|
{
|
2026-04-30 17:18:17 +08:00
|
|
|
|
_entryListContainer = new IMGUIContainer(DrawEntryList);
|
|
|
|
|
|
_entryListContainer.style.flexGrow = 1f;
|
|
|
|
|
|
_entryListContainer.style.marginBottom = 4f;
|
|
|
|
|
|
_leftPane.Add(_entryListContainer);
|
2026-04-17 21:01:20 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private void BuildRightPane(VisualElement rightPane)
|
|
|
|
|
|
{
|
|
|
|
|
|
_emptyContainer = new VisualElement
|
|
|
|
|
|
{
|
|
|
|
|
|
style =
|
|
|
|
|
|
{
|
|
|
|
|
|
flexGrow = 1f,
|
|
|
|
|
|
justifyContent = Justify.Center
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
2026-04-30 17:18:17 +08:00
|
|
|
|
_emptyContainer.Add(new HelpBox("当前没有选中任何规则。", HelpBoxMessageType.Info));
|
2026-04-17 21:01:20 +08:00
|
|
|
|
|
|
|
|
|
|
_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);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-30 17:18:17 +08:00
|
|
|
|
private void SetAsset(PoolConfigScriptableObject asset)
|
2026-04-17 21:01:20 +08:00
|
|
|
|
{
|
2026-04-30 17:18:17 +08:00
|
|
|
|
_asset = asset;
|
|
|
|
|
|
_assetGuid = GetAssetGuid(asset);
|
|
|
|
|
|
_selectedIndex = 0;
|
|
|
|
|
|
_hasUnsavedChanges = false;
|
|
|
|
|
|
_entryListScrollPosition = Vector2.zero;
|
|
|
|
|
|
RebindAssetState();
|
|
|
|
|
|
RefreshUi();
|
2026-04-17 21:01:20 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-30 17:18:17 +08:00
|
|
|
|
private void RestoreWindowState()
|
2026-04-17 21:01:20 +08:00
|
|
|
|
{
|
2026-04-30 17:18:17 +08:00
|
|
|
|
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();
|
2026-04-17 21:01:20 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-30 17:18:17 +08:00
|
|
|
|
private void RebindAssetState()
|
2026-04-17 21:01:20 +08:00
|
|
|
|
{
|
2026-04-30 17:18:17 +08:00
|
|
|
|
_entryList = null;
|
2026-04-17 21:01:20 +08:00
|
|
|
|
|
|
|
|
|
|
if (_asset == null)
|
|
|
|
|
|
{
|
2026-04-30 17:18:17 +08:00
|
|
|
|
_assetGuid = string.Empty;
|
2026-04-17 21:01:20 +08:00
|
|
|
|
_serializedObject = null;
|
|
|
|
|
|
_entriesProperty = null;
|
2026-04-30 17:18:17 +08:00
|
|
|
|
_entryIndices.Clear();
|
|
|
|
|
|
return;
|
2026-04-17 21:01:20 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-30 17:18:17 +08:00
|
|
|
|
_asset.Normalize();
|
|
|
|
|
|
_serializedObject = new SerializedObject(_asset);
|
|
|
|
|
|
_entriesProperty = _serializedObject.FindProperty("entries");
|
|
|
|
|
|
RebuildEntryIndices();
|
|
|
|
|
|
ClampSelection();
|
2026-04-17 21:01:20 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private void RefreshUi()
|
|
|
|
|
|
{
|
|
|
|
|
|
if (_titleLabel == null)
|
|
|
|
|
|
{
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
RefreshTitle();
|
|
|
|
|
|
_saveButton?.SetEnabled(_asset != null);
|
|
|
|
|
|
|
|
|
|
|
|
if (_asset == null || _serializedObject == null)
|
|
|
|
|
|
{
|
|
|
|
|
|
_entryIndices.Clear();
|
2026-04-30 17:18:17 +08:00
|
|
|
|
_entryList = null;
|
|
|
|
|
|
_entryListContainer?.MarkDirtyRepaint();
|
2026-04-17 21:01:20 +08:00
|
|
|
|
_emptyContainer.Clear();
|
2026-04-30 17:18:17 +08:00
|
|
|
|
_emptyContainer.Add(new HelpBox("请选择或双击 PoolConfig 资源,然后在这个窗口里编辑对象池规则。", HelpBoxMessageType.Info));
|
2026-04-17 21:01:20 +08:00
|
|
|
|
_emptyContainer.style.display = DisplayStyle.Flex;
|
|
|
|
|
|
_detailScrollView.style.display = DisplayStyle.None;
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
RebuildEntryIndices();
|
|
|
|
|
|
ClampSelection();
|
2026-04-30 17:18:17 +08:00
|
|
|
|
EnsureEntryReorderableList();
|
|
|
|
|
|
SyncEntryListSelection();
|
|
|
|
|
|
_entryListContainer?.MarkDirtyRepaint();
|
2026-04-17 21:01:20 +08:00
|
|
|
|
|
|
|
|
|
|
if (_entryIndices.Count > 0)
|
|
|
|
|
|
{
|
|
|
|
|
|
_emptyContainer.style.display = DisplayStyle.None;
|
|
|
|
|
|
_detailScrollView.style.display = DisplayStyle.Flex;
|
|
|
|
|
|
RebuildDetailFields();
|
|
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
|
|
|
|
|
_emptyContainer.Clear();
|
2026-04-30 17:18:17 +08:00
|
|
|
|
_emptyContainer.Add(new HelpBox("当前没有可编辑的规则,请先新增一条对象池规则。", HelpBoxMessageType.Info));
|
2026-04-17 21:01:20 +08:00
|
|
|
|
_emptyContainer.style.display = DisplayStyle.Flex;
|
|
|
|
|
|
_detailScrollView.style.display = DisplayStyle.None;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private void RebuildEntryIndices()
|
|
|
|
|
|
{
|
|
|
|
|
|
_entryIndices.Clear();
|
|
|
|
|
|
int count = _entriesProperty?.arraySize ?? 0;
|
|
|
|
|
|
for (int i = 0; i < count; i++)
|
|
|
|
|
|
{
|
|
|
|
|
|
_entryIndices.Add(i);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-30 17:18:17 +08:00
|
|
|
|
private void DrawEntryList()
|
|
|
|
|
|
{
|
|
|
|
|
|
if (_asset == null || _serializedObject == null || _entriesProperty == null)
|
|
|
|
|
|
{
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
EnsureEntryReorderableList();
|
|
|
|
|
|
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();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private void OnEntryListAdd(ReorderableList list)
|
|
|
|
|
|
{
|
|
|
|
|
|
AddEntry();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private void OnEntryListRemove(ReorderableList list)
|
2026-04-17 21:01:20 +08:00
|
|
|
|
{
|
2026-04-30 17:18:17 +08:00
|
|
|
|
RemoveEntry();
|
2026-04-17 21:01:20 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-30 17:18:17 +08:00
|
|
|
|
private void OnEntryListReordered(ReorderableList list)
|
2026-04-17 21:01:20 +08:00
|
|
|
|
{
|
2026-04-30 17:18:17 +08:00
|
|
|
|
if (_asset == null || _serializedObject == null || _entriesProperty == null)
|
2026-04-17 21:01:20 +08:00
|
|
|
|
{
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-30 17:18:17 +08:00
|
|
|
|
RefreshEntryPriorities();
|
|
|
|
|
|
_serializedObject.ApplyModifiedPropertiesWithoutUndo();
|
|
|
|
|
|
_asset.Normalize();
|
|
|
|
|
|
RebuildEntryIndices();
|
|
|
|
|
|
_selectedIndex = Mathf.Clamp(list.index, 0, _entryIndices.Count - 1);
|
|
|
|
|
|
_hasUnsavedChanges = true;
|
|
|
|
|
|
RefreshTitle();
|
2026-04-17 21:01:20 +08:00
|
|
|
|
RebuildDetailFields();
|
2026-04-30 17:18:17 +08:00
|
|
|
|
_entryListContainer?.MarkDirtyRepaint();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private void SyncEntryListSelection()
|
|
|
|
|
|
{
|
|
|
|
|
|
if (_entryList == null)
|
|
|
|
|
|
{
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
_entryList.index = _entryIndices.Count == 0 ? -1 : _selectedIndex;
|
2026-04-17 21:01:20 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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))
|
|
|
|
|
|
{
|
2026-04-30 17:18:17 +08:00
|
|
|
|
SerializedProperty currentProperty = iterator.Copy();
|
|
|
|
|
|
if (!ShouldDisplayField(currentProperty.name))
|
|
|
|
|
|
{
|
|
|
|
|
|
enterChildren = false;
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
_detailFieldsContainer.Add(CreateDetailField(currentProperty));
|
2026-04-17 21:01:20 +08:00
|
|
|
|
enterChildren = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
_detailFieldsContainer.Bind(_serializedObject);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-30 17:18:17 +08:00
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-17 21:01:20 +08:00
|
|
|
|
private void OnDetailPropertyChanged(SerializedPropertyChangeEvent evt)
|
2026-04-30 17:18:17 +08:00
|
|
|
|
{
|
|
|
|
|
|
ApplyDetailPropertyChanges();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private void ApplyDetailPropertyChanges()
|
2026-04-17 21:01:20 +08:00
|
|
|
|
{
|
|
|
|
|
|
if (_asset == null || _serializedObject == null)
|
|
|
|
|
|
{
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
_serializedObject.ApplyModifiedPropertiesWithoutUndo();
|
|
|
|
|
|
_asset.Normalize();
|
|
|
|
|
|
_hasUnsavedChanges = true;
|
2026-04-30 17:18:17 +08:00
|
|
|
|
_entryListContainer?.MarkDirtyRepaint();
|
2026-04-17 21:01:20 +08:00
|
|
|
|
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);
|
2026-04-30 17:18:17 +08:00
|
|
|
|
RefreshEntryPriorities();
|
2026-04-17 21:01:20 +08:00
|
|
|
|
|
|
|
|
|
|
_serializedObject.ApplyModifiedPropertiesWithoutUndo();
|
|
|
|
|
|
_asset.Normalize();
|
|
|
|
|
|
RebuildEntryIndices();
|
|
|
|
|
|
_selectedIndex = index;
|
|
|
|
|
|
_hasUnsavedChanges = true;
|
|
|
|
|
|
RefreshUi();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private void RemoveEntry()
|
|
|
|
|
|
{
|
|
|
|
|
|
if (_entriesProperty == null || _entryIndices.Count == 0)
|
|
|
|
|
|
{
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
_serializedObject.Update();
|
|
|
|
|
|
_entriesProperty.DeleteArrayElementAtIndex(_selectedIndex);
|
2026-04-30 17:18:17 +08:00
|
|
|
|
RefreshEntryPriorities();
|
2026-04-17 21:01:20 +08:00
|
|
|
|
_serializedObject.ApplyModifiedPropertiesWithoutUndo();
|
|
|
|
|
|
_asset.Normalize();
|
|
|
|
|
|
RebuildEntryIndices();
|
|
|
|
|
|
|
|
|
|
|
|
if (_selectedIndex >= _entryIndices.Count)
|
|
|
|
|
|
{
|
|
|
|
|
|
_selectedIndex = Mathf.Max(0, _entryIndices.Count - 1);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
_hasUnsavedChanges = true;
|
|
|
|
|
|
RefreshUi();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private void InitializeNewEntry(SerializedProperty property, int index)
|
|
|
|
|
|
{
|
2026-04-30 17:18:17 +08:00
|
|
|
|
property.FindPropertyRelative("entryName").stringValue = Utility.Text.Format("对象池规则{0}", index + 1);
|
2026-04-17 21:01:20 +08:00
|
|
|
|
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;
|
2026-04-30 17:18:17 +08:00
|
|
|
|
property.FindPropertyRelative("category").enumValueIndex = (int)PoolCategory.Default;
|
2026-04-17 21:01:20 +08:00
|
|
|
|
property.FindPropertyRelative("softCapacity").intValue = 8;
|
2026-04-30 17:18:17 +08:00
|
|
|
|
property.FindPropertyRelative("hardCapacity").intValue = 16;
|
2026-04-17 21:01:20 +08:00
|
|
|
|
property.FindPropertyRelative("priority").intValue = index;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-30 17:18:17 +08:00
|
|
|
|
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);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-17 21:01:20 +08:00
|
|
|
|
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()
|
|
|
|
|
|
{
|
2026-04-30 17:18:17 +08:00
|
|
|
|
string assetLabel = _asset == null ? "未选择 PoolConfig" : _asset.name;
|
2026-04-17 21:01:20 +08:00
|
|
|
|
if (_hasUnsavedChanges)
|
|
|
|
|
|
{
|
2026-04-30 17:18:17 +08:00
|
|
|
|
assetLabel = Utility.Text.Format("{0} *", assetLabel);
|
2026-04-17 21:01:20 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
_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)
|
|
|
|
|
|
{
|
2026-04-30 17:18:17 +08:00
|
|
|
|
return "<规则缺失>";
|
2026-04-17 21:01:20 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
string entryName = property.FindPropertyRelative("entryName").stringValue;
|
|
|
|
|
|
string assetPath = property.FindPropertyRelative("assetPath").stringValue;
|
|
|
|
|
|
if (!string.IsNullOrWhiteSpace(entryName))
|
|
|
|
|
|
{
|
|
|
|
|
|
return entryName;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-30 17:18:17 +08:00
|
|
|
|
return string.IsNullOrWhiteSpace(assetPath) ? "<未命名规则>" : assetPath;
|
2026-04-17 21:01:20 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-30 17:18:17 +08:00
|
|
|
|
private static List<string> GetEnumOptions(string propertyName)
|
2026-04-17 21:01:20 +08:00
|
|
|
|
{
|
2026-04-30 17:18:17 +08:00
|
|
|
|
return propertyName switch
|
2026-04-17 21:01:20 +08:00
|
|
|
|
{
|
2026-04-30 17:18:17 +08:00
|
|
|
|
"matchMode" => MatchModeOptions,
|
|
|
|
|
|
"loaderType" => LoaderTypeOptions,
|
|
|
|
|
|
_ => null
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
2026-04-17 21:01:20 +08:00
|
|
|
|
|
2026-04-30 17:18:17 +08:00
|
|
|
|
private static bool ShouldDisplayField(string propertyName)
|
|
|
|
|
|
{
|
|
|
|
|
|
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
|
|
|
|
|
|
};
|
2026-04-17 21:01:20 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private void ClampSelection()
|
|
|
|
|
|
{
|
|
|
|
|
|
if (_entryIndices.Count == 0)
|
|
|
|
|
|
{
|
|
|
|
|
|
_selectedIndex = 0;
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
_selectedIndex = Mathf.Clamp(_selectedIndex, 0, _entryIndices.Count - 1);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|