com.alicizax.unity.framework/Editor/GameObjectPool/PoolConfigEditorWindow.cs

780 lines
27 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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