From 550f588408c96d5813d1838d00555e093ca4337d Mon Sep 17 00:00:00 2001 From: LiRuoChen <571244399@qq.com> Date: Tue, 28 Jul 2026 13:50:44 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E6=8D=A2=E8=A1=A8=E6=A0=BC=E5=AF=B9?= =?UTF-8?q?=E8=B1=A1=E6=B1=A0=E7=B3=BB=E7=BB=9F=EF=BC=8C=E6=96=B0=E7=9A=84?= =?UTF-8?q?=E5=AF=B9=E8=B1=A1=E6=B1=A0=E7=B3=BB=E7=BB=9F=E4=BC=9A=E5=B0=BD?= =?UTF-8?q?=E5=8F=AF=E8=83=BD=E5=87=8F=E5=B0=8F=E5=AF=B9=E8=B1=A1=E7=9A=84?= =?UTF-8?q?=E5=9B=9E=E6=94=B6=E6=93=8D=E4=BD=9C=EF=BC=8C=E5=90=8C=E6=97=B6?= =?UTF-8?q?=E5=BC=95=E5=85=A5=E5=9F=BA=E4=BA=8E=E6=9B=BC=E5=93=88=E9=A1=BF?= =?UTF-8?q?=E7=82=B9=E8=B7=9D=E7=9A=84=E5=AF=B9=E8=B1=A1=E6=B1=A0=E7=B4=A2?= =?UTF-8?q?=E5=BC=95=E9=80=BB=E8=BE=91=EF=BC=8C=E5=BC=95=E5=85=A5=E8=AE=A1?= =?UTF-8?q?=E6=95=B0=E5=99=A8=EF=BC=8C=E9=81=BF=E5=85=8D=E9=87=8E=E5=AF=B9?= =?UTF-8?q?=E8=B1=A1=E6=88=96=E5=AF=B9=E8=B1=A1=E5=86=B2=E7=AA=81=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Rendering/Component/TableRenderer.cs | 30 +-- .../XericUIActionTable.RendererFilter.cs | 39 ++-- .../Rendering/Elements/CellAssembler.cs | 134 ++++------- .../Rendering/Elements/PrefabPrototypePool.cs | 93 ++++---- .../Rendering/Elements/SlotCounterPool.cs | 214 ++++++++++++++++++ .../Elements/SlotCounterPool.cs.meta | 11 + 6 files changed, 351 insertions(+), 170 deletions(-) create mode 100644 Runtime/XTable/Rendering/Elements/SlotCounterPool.cs create mode 100644 Runtime/XTable/Rendering/Elements/SlotCounterPool.cs.meta diff --git a/Runtime/XTable/Rendering/Component/TableRenderer.cs b/Runtime/XTable/Rendering/Component/TableRenderer.cs index bad2e8b..21e9bb6 100644 --- a/Runtime/XTable/Rendering/Component/TableRenderer.cs +++ b/Runtime/XTable/Rendering/Component/TableRenderer.cs @@ -229,11 +229,12 @@ namespace XericUI.XTable.Rendering.Component XTableCellData data = tableData?.GetCell(r, c); string cellNS = (data != null && !string.IsNullOrEmpty(data.StyleNamespace)) ? data.StyleNamespace : defaultNs; + int cellId = r * colWidths.Length + c; // 选择高亮 if (IsInSelectionRange != null && IsInSelectionRange(r, c)) { - var selImg = m_Assembler.GetImage("cell_multiselect"); + var selImg = m_Assembler.GetImage(cellId, "cell_multiselect"); if (selImg != null) { selImg.Rect.anchoredPosition = new Vector2(cellX, cellY); @@ -243,7 +244,7 @@ namespace XericUI.XTable.Rendering.Component } else if (r == selRow && c == selCol) { - var selImg = m_Assembler.GetImage("cell_select"); + var selImg = m_Assembler.GetImage(cellId, "cell_select"); if (selImg != null) { selImg.Rect.anchoredPosition = new Vector2(cellX, cellY); @@ -253,7 +254,7 @@ namespace XericUI.XTable.Rendering.Component } // 网格线 - var hLine = m_Assembler.GetImage("cell_hline"); + var hLine = m_Assembler.GetImage(cellId, "cell_hline"); if (hLine != null) { hLine.Rect.anchoredPosition = new Vector2(cellX, cellY - cellH); @@ -261,7 +262,7 @@ namespace XericUI.XTable.Rendering.Component hLine.Image.color = borderColor; } - var vLine = m_Assembler.GetImage("cell_vline"); + var vLine = m_Assembler.GetImage(cellId, "cell_vline"); if (vLine != null) { vLine.Rect.anchoredPosition = new Vector2(cellX + cellW - borderWidth, cellY); @@ -276,7 +277,7 @@ namespace XericUI.XTable.Rendering.Component case CellContentType.Text: if (!string.IsNullOrEmpty(data.Text)) { - var txt = m_Assembler.GetText("cell_text", cellNS); + var txt = m_Assembler.GetText(cellId, "cell_text", cellNS); if (txt != null) { txt.Rect.anchoredPosition = new Vector2(cellX + CellPadding, cellY); @@ -296,7 +297,7 @@ namespace XericUI.XTable.Rendering.Component if (data.Image != null) { Sprite sprite = GetOrCreateSprite(data.Image); - var img = m_Assembler.GetImage("cell_image", cellNS); + var img = m_Assembler.GetImage(cellId, "cell_image", cellNS); if (img != null) { img.Rect.anchoredPosition = new Vector2(cellX + CellPadding, cellY); @@ -308,7 +309,7 @@ namespace XericUI.XTable.Rendering.Component case CellContentType.Prefab: if (data.Prefab != null && PrefabPool != null) { - GameObject instance = PrefabPool.Acquire(data.Prefab, r, c); + GameObject instance = PrefabPool.Acquire(data.Prefab, cellId); if (instance != null) { RectTransform prt = instance.GetComponent(); @@ -357,10 +358,11 @@ namespace XericUI.XTable.Rendering.Component XTableCellData data = tableData?.GetCell(r, c); string cellNS = (data != null && !string.IsNullOrEmpty(data.StyleNamespace)) ? data.StyleNamespace : defaultNs; + int cellId = r * colWidths.Length + c; if (IsInSelectionRange != null && IsInSelectionRange(r, c)) { - var selImg = m_Assembler.GetImage("cell_multiselect"); + var selImg = m_Assembler.GetImage(cellId, "cell_multiselect"); if (selImg != null) { selImg.Rect.anchoredPosition = new Vector2(cellX, cellY); @@ -370,7 +372,7 @@ namespace XericUI.XTable.Rendering.Component } else if (r == selRow && c == selCol) { - var selImg = m_Assembler.GetImage("cell_select"); + var selImg = m_Assembler.GetImage(cellId, "cell_select"); if (selImg != null) { selImg.Rect.anchoredPosition = new Vector2(cellX, cellY); @@ -379,7 +381,7 @@ namespace XericUI.XTable.Rendering.Component } } - var hLine = m_Assembler.GetImage("cell_hline"); + var hLine = m_Assembler.GetImage(cellId, "cell_hline"); if (hLine != null) { hLine.Rect.anchoredPosition = new Vector2(cellX, cellY - cellH); @@ -387,7 +389,7 @@ namespace XericUI.XTable.Rendering.Component hLine.Image.color = Config.RuntimeBorderColor; } - var vLine = m_Assembler.GetImage("cell_vline"); + var vLine = m_Assembler.GetImage(cellId, "cell_vline"); if (vLine != null) { vLine.Rect.anchoredPosition = new Vector2(cellX + cellW - Config.RuntimeBorderWidth, cellY); @@ -402,7 +404,7 @@ namespace XericUI.XTable.Rendering.Component case CellContentType.Text: if (!string.IsNullOrEmpty(data.Text)) { - var txt = m_Assembler.GetText("cell_text", cellNS); + var txt = m_Assembler.GetText(cellId, "cell_text", cellNS); if (txt != null) { txt.Rect.anchoredPosition = new Vector2(cellX + CellPadding, cellY); @@ -415,7 +417,7 @@ namespace XericUI.XTable.Rendering.Component if (data.Image != null) { Sprite sprite = GetOrCreateSprite(data.Image); - var img = m_Assembler.GetImage("cell_image", cellNS); + var img = m_Assembler.GetImage(cellId, "cell_image", cellNS); if (img != null) { img.Rect.anchoredPosition = new Vector2(cellX + CellPadding, cellY); @@ -427,7 +429,7 @@ namespace XericUI.XTable.Rendering.Component case CellContentType.Prefab: if (data.Prefab != null && PrefabPool != null) { - GameObject instance = PrefabPool.Acquire(data.Prefab, r, c); + GameObject instance = PrefabPool.Acquire(data.Prefab, cellId); if (instance != null) { RectTransform prt = instance.GetComponent(); diff --git a/Runtime/XTable/Rendering/Component/XericUIActionTable.RendererFilter.cs b/Runtime/XTable/Rendering/Component/XericUIActionTable.RendererFilter.cs index 93a4f23..f5d9762 100644 --- a/Runtime/XTable/Rendering/Component/XericUIActionTable.RendererFilter.cs +++ b/Runtime/XTable/Rendering/Component/XericUIActionTable.RendererFilter.cs @@ -154,14 +154,15 @@ namespace XericUI.XTable.Rendering.Component cellH += m_RowHeights[row + i]; } + int cellId = row * m_ColWidths.Length + col; var objs = new CellTrackedObjs(); // ── 选择高亮 ── - ApplySelectionHighlight(row, col, cellX, cellY, cellW, cellH, cellNS, editorMode, ref objs); + ApplySelectionHighlight(cellId, row, col, cellX, cellY, cellW, cellH, cellNS, editorMode, ref objs); // ── 网格线(仅完整刷新时)── if (withGridLines) - ApplyGridLines(cellX, cellY, cellW, cellH, cellNS, editorMode, ref objs); + ApplyGridLines(cellId, cellX, cellY, cellW, cellH, cellNS, editorMode, ref objs); // ── 单元格内容 ── if (data != null) @@ -169,13 +170,13 @@ namespace XericUI.XTable.Rendering.Component switch (data.ContentType) { case CellContentType.Text: - RenderCellText(data, cellX, cellY, cellW, cellH, cellNS, editorMode, ref objs); + RenderCellText(cellId, data, cellX, cellY, cellW, cellH, cellNS, editorMode, ref objs); break; case CellContentType.Image: - RenderCellImage(data, cellX, cellY, cellW, cellH, cellNS, ref objs); + RenderCellImage(cellId, data, cellX, cellY, cellW, cellH, cellNS, ref objs); break; case CellContentType.Prefab: - RenderCellPrefab(data, row, col, cellX, cellY, cellW, cellH); + RenderCellPrefab(cellId, data, row, col, cellX, cellY, cellW, cellH); break; } } @@ -184,7 +185,7 @@ namespace XericUI.XTable.Rendering.Component m_LastRenderedCells.Add((row, col)); } - private void ApplySelectionHighlight(int row, int col, + private void ApplySelectionHighlight(int cellId, int row, int col, float cellX, float cellY, float cellW, float cellH, string cellNS, bool editorMode, ref CellTrackedObjs objs) { @@ -194,7 +195,7 @@ namespace XericUI.XTable.Rendering.Component if (!inRange && !isFocused) return; string key = inRange ? "cell_multiselect" : "cell_select"; - var img = m_Assembler.GetImage(key, cellNS); + var img = m_Assembler.GetImage(cellId, key, cellNS); if (img == null) return; img.Rect.anchoredPosition = new Vector2(cellX, cellY); img.Rect.sizeDelta = new Vector2(cellW, cellH); @@ -222,7 +223,7 @@ namespace XericUI.XTable.Rendering.Component objs.SelectBg = img; } - private void ApplyGridLines(float cellX, float cellY, float cellW, float cellH, + private void ApplyGridLines(int cellId, float cellX, float cellY, float cellW, float cellH, string cellNS, bool editorMode, ref CellTrackedObjs objs) { Color borderColor; @@ -240,7 +241,7 @@ namespace XericUI.XTable.Rendering.Component } // 水平网格线(底部) - var hLine = m_Assembler.GetImage("cell_hline", cellNS); + var hLine = m_Assembler.GetImage(cellId, "cell_hline", cellNS); if (hLine != null) { hLine.Rect.anchoredPosition = new Vector2(cellX, cellY - cellH); @@ -250,7 +251,7 @@ namespace XericUI.XTable.Rendering.Component } // 垂直网格线(右侧) - var vLine = m_Assembler.GetImage("cell_vline", cellNS); + var vLine = m_Assembler.GetImage(cellId, "cell_vline", cellNS); if (vLine != null) { vLine.Rect.anchoredPosition = new Vector2(cellX + cellW - borderWidth, cellY); @@ -260,13 +261,13 @@ namespace XericUI.XTable.Rendering.Component } } - private void RenderCellText(XTableCellData data, + private void RenderCellText(int cellId, XTableCellData data, float cellX, float cellY, float cellW, float cellH, string cellNS, bool editorMode, ref CellTrackedObjs objs) { if (string.IsNullOrEmpty(data.Text)) return; - var txt = m_Assembler.GetText("cell_text", cellNS); + var txt = m_Assembler.GetText(cellId, "cell_text", cellNS); if (txt == null) return; txt.Rect.anchoredPosition = new Vector2(cellX + CellPadding, cellY); txt.Rect.sizeDelta = new Vector2(cellW - CellPadding2, cellH); @@ -283,14 +284,14 @@ namespace XericUI.XTable.Rendering.Component objs.CellText = txt; } - private void RenderCellImage(XTableCellData data, + private void RenderCellImage(int cellId, XTableCellData data, float cellX, float cellY, float cellW, float cellH, string cellNS, ref CellTrackedObjs objs) { if (data.Image == null) return; Sprite sprite = GetOrCreateSprite(data.Image); - var img = m_Assembler.GetImage("cell_image", cellNS); + var img = m_Assembler.GetImage(cellId, "cell_image", cellNS); if (img == null) return; img.Rect.anchoredPosition = new Vector2(cellX + CellPadding, cellY); img.Rect.sizeDelta = new Vector2(cellW - CellPadding2, cellH); @@ -299,12 +300,12 @@ namespace XericUI.XTable.Rendering.Component objs.CellImage = img; } - private void RenderCellPrefab(XTableCellData data, int row, int col, + private void RenderCellPrefab(int cellId, XTableCellData data, int row, int col, float cellX, float cellY, float cellW, float cellH) { if (data.Prefab == null) return; - GameObject instance = m_PrefabPool.Acquire(data.Prefab, row, col); + GameObject instance = m_PrefabPool.Acquire(data.Prefab, cellId); if (instance == null) return; RectTransform prt = instance.GetComponent(); prt.anchoredPosition = new Vector2(cellX, cellY); @@ -442,7 +443,7 @@ namespace XericUI.XTable.Rendering.Component ReleaseTrackedCell(cell.Item1, cell.Item2); } - // 3. 回收不再可见的预制体 + // 3. 回收不再可见的预制体(仅从映射表移除,预制体保留在计数器槽位中复用) if (m_ActivePrefabMap != null && m_ActivePrefabMap.Count > 0) { m_ReusableCellList.Clear(); @@ -452,9 +453,7 @@ namespace XericUI.XTable.Rendering.Component foreach (var key in m_ReusableCellList) { - var cellData = TableData.GetCell(key.Item1, key.Item2); - cellData?.OnPrefabRelease?.Invoke(m_ActivePrefabMap[key]); - m_PrefabPool.Release(m_ActivePrefabMap[key]); + // 不调用 OnPrefabRelease 和 Release——预制体保留在槽位中 m_ActivePrefabMap.Remove(key); } } diff --git a/Runtime/XTable/Rendering/Elements/CellAssembler.cs b/Runtime/XTable/Rendering/Elements/CellAssembler.cs index 7e5b636..bfa90db 100644 --- a/Runtime/XTable/Rendering/Elements/CellAssembler.cs +++ b/Runtime/XTable/Rendering/Elements/CellAssembler.cs @@ -1,5 +1,3 @@ -using System.Collections.Generic; - using TMPro; using UnityEngine; @@ -8,7 +6,8 @@ using UnityEngine.UI; namespace XericUI.XTable.Rendering.Elements { /// - /// 表格单元动态拼装器——通过对象池管理 Image + TMP_Text 的组合创建与回收。 + /// 表格单元动态拼装器——通过 SlotCounterPool 管理 Image + TMP_Text 的组合创建与回收。 + /// 每个单元格通过 cellId 映射到固定索引的计数器槽位,空闲槽位经 FrozenSlots 全局流转复用。 /// 所有元素统一使用 anchor=(0,1) pivot=(0,1) 左上角坐标系。 /// 所有动态生成对象标记 HideFlags.DontSave | HideFlags.HideInHierarchy,防止泄漏到场景。 /// @@ -22,6 +21,8 @@ namespace XericUI.XTable.Rendering.Elements public RectTransform Rect; public Image Image; public StyleBoundElement Style; + /// 绑定的单元格 ID,用于 Release 定位槽位 + public int CellId; } public class PooledText @@ -30,6 +31,8 @@ namespace XericUI.XTable.Rendering.Elements public RectTransform Rect; public TMP_Text Text; public StyleBoundElement Style; + /// 绑定的单元格 ID,用于 Release 定位槽位 + public int CellId; } #endregion @@ -44,20 +47,14 @@ namespace XericUI.XTable.Rendering.Elements /// 所有已绑定的 StyleBoundElement 回调集合(用于样式变更时通知外部) public event System.Action OnStyleChangeDetected; - private Queue m_ImagePool = new Queue(); - private Queue m_TextPool = new Queue(); - private List m_ActiveImages = new List(); - private List m_ActiveTexts = new List(); + /// 基于计数器的 Image 槽位对象池 + private SlotCounterPool m_ImagePool; + /// 基于计数器的 Text 槽位对象池 + private SlotCounterPool m_TextPool; /// HideFlags 应用于所有动态创建的对象(不保存、不在 Hierarchy 显示) private const HideFlags POOL_FLAGS = HideFlags.DontSave | HideFlags.HideInHierarchy; - /// 渲染项目总数上限,超出后 GetImage/GetText 返回 null 并发出警告 - private const int MAX_POOL_ITEMS = 256; - - private int m_ImageCreatedCount; - private int m_TextCreatedCount; - #endregion #region 构造函数与析构 @@ -66,33 +63,15 @@ namespace XericUI.XTable.Rendering.Elements { m_Parent = parent; m_DefaultStyleNamespace = defaultStyleNamespace; + m_ImagePool = new SlotCounterPool(() => CreateImage("cell_img")); + m_TextPool = new SlotCounterPool(() => CreateText("cell_text")); } - /// 彻底销毁所有池对象(包括活跃和休眠的) + /// 彻底销毁所有池对象(包括计数器内活跃槽位和冻结槽位) public void DestroyAll() { - // 杀死活跃对象 - for (int i = m_ActiveImages.Count - 1; i >= 0; i--) - { - KillObject(m_ActiveImages[i].Root); - } - m_ActiveImages.Clear(); - - for (int i = m_ActiveTexts.Count - 1; i >= 0; i--) - { - KillObject(m_ActiveTexts[i].Root); - } - m_ActiveTexts.Clear(); - - // 杀死池中对象 - while (m_ImagePool.Count > 0) - { - KillObject(m_ImagePool.Dequeue().Root); - } - while (m_TextPool.Count > 0) - { - KillObject(m_TextPool.Dequeue().Root); - } + m_ImagePool.DestroyAll(item => KillObject(item.Root)); + m_TextPool.DestroyAll(item => KillObject(item.Root)); } private static void KillObject(GameObject go) @@ -116,49 +95,29 @@ namespace XericUI.XTable.Rendering.Elements #region 池操作 - public PooledImage GetImage(string name = "cell_img", string styleNamespace = null) + /// 为指定 cellId 获取一个池化 Image + public PooledImage GetImage(int cellId, string name = "cell_img", string styleNamespace = null) { - PooledImage item; - if (m_ImagePool.Count > 0) - { - item = m_ImagePool.Dequeue(); - ResetImageDefaults(item); - } - else if (m_ImageCreatedCount < MAX_POOL_ITEMS) - { - item = CreateImage(name); - m_ImageCreatedCount++; - } - else - { - Debug.LogWarning($"[XericUIActionTable] Image 池已达上限 {MAX_POOL_ITEMS},无法分配新对象"); - return null; - } + var item = m_ImagePool.Acquire(cellId); + if (item == null) return null; + + item.CellId = cellId; + item.Root.name = name; + ResetImageDefaults(item); BindStyle(item.Style, styleNamespace); - m_ActiveImages.Add(item); return item; } - public PooledText GetText(string name = "cell_text", string styleNamespace = null) + /// 为指定 cellId 获取一个池化 TMP_Text + public PooledText GetText(int cellId, string name = "cell_text", string styleNamespace = null) { - PooledText item; - if (m_TextPool.Count > 0) - { - item = m_TextPool.Dequeue(); - ResetTextDefaults(item); - } - else if (m_TextCreatedCount < MAX_POOL_ITEMS) - { - item = CreateText(name); - m_TextCreatedCount++; - } - else - { - Debug.LogWarning($"[XericUIActionTable] Text 池已达上限 {MAX_POOL_ITEMS},无法分配新对象"); - return null; - } + var item = m_TextPool.Acquire(cellId); + if (item == null) return null; + + item.CellId = cellId; + item.Root.name = name; + ResetTextDefaults(item); BindStyle(item.Style, styleNamespace); - m_ActiveTexts.Add(item); return item; } @@ -183,41 +142,28 @@ namespace XericUI.XTable.Rendering.Elements OnStyleChangeDetected?.Invoke(); } + /// 归还所有槽位至 FrozenSlots(全量刷新时调用) public void ReturnAll() { - foreach (var img in m_ActiveImages) ReturnImage(img); - m_ActiveImages.Clear(); - - foreach (var txt in m_ActiveTexts) ReturnText(txt); - m_ActiveTexts.Clear(); + // Unbind 所有活跃槽位的样式回调 + m_ImagePool.ReturnAll(); + m_TextPool.ReturnAll(); } - /// 归还单个 Image 池对象(从活跃列表移除并推入休眠栈) + /// 归还单个 Image 池对象(通过 CellId 定位槽位并移入 FrozenSlots) public void ReleaseImage(PooledImage item) { if (item == null) return; - m_ActiveImages.Remove(item); - ReturnImage(item); + item.Style?.Unbind(); + m_ImagePool.Release(item.CellId); } - /// 归还单个 Text 池对象(从活跃列表移除并推入休眠栈) + /// 归还单个 Text 池对象(通过 CellId 定位槽位并移入 FrozenSlots) public void ReleaseText(PooledText item) { if (item == null) return; - m_ActiveTexts.Remove(item); - ReturnText(item); - } - - private void ReturnImage(PooledImage item) - { item.Style?.Unbind(); - m_ImagePool.Enqueue(item); - } - - private void ReturnText(PooledText item) - { - item.Style?.Unbind(); - m_TextPool.Enqueue(item); + m_TextPool.Release(item.CellId); } #endregion diff --git a/Runtime/XTable/Rendering/Elements/PrefabPrototypePool.cs b/Runtime/XTable/Rendering/Elements/PrefabPrototypePool.cs index ee3e0b6..436aba7 100644 --- a/Runtime/XTable/Rendering/Elements/PrefabPrototypePool.cs +++ b/Runtime/XTable/Rendering/Elements/PrefabPrototypePool.cs @@ -5,8 +5,8 @@ using UnityEngine; namespace XericUI.XTable.Rendering.Elements { /// - /// 预制体对象池——按原型分组管理,每个预制体对应一个独立的对象池。 - /// 使用 Queue + HashSet 模式,FIFO 保证对象按回收顺序均匀复用。 + /// 预制体对象池——按原型分组管理,每个原型使用计数器槽位绑定 cellId 到预制体实例。 + /// 预制体不参与 FrozenSlots 流转:始终绑定在 cellId 槽位上,仅在表格销毁时释放。 /// public class PrefabPrototypePool { @@ -15,10 +15,8 @@ namespace XericUI.XTable.Rendering.Elements private class PrefabPool { public GameObject Prototype; - /// 休眠实例队列(FIFO,先归还的优先复用) - public Queue Available = new Queue(); - /// 活跃实例集合(O(1) 查找 + 防止重复归还) - public HashSet Active = new HashSet(); + /// cellId → 预制体实例 的计数器槽位绑定 + public Dictionary Slots = new Dictionary(); } #endregion @@ -28,7 +26,7 @@ namespace XericUI.XTable.Rendering.Elements private Dictionary m_Pools = new Dictionary(); private Transform m_Parent; - /// 渲染项目总数上限 + /// 渲染项目总数上限(全局,跨所有原型) private const int MAX_PREFAB_ITEMS = 256; private int m_TotalCreated; @@ -46,21 +44,18 @@ namespace XericUI.XTable.Rendering.Elements m_Parent = parent; } - /// 彻底销毁所有池对象(活跃 + 休眠) + /// 彻底销毁所有池对象(所有原型的活跃槽位) public void DestroyAll() { foreach (var kv in m_Pools) { var pool = kv.Value; - // 销毁活跃实例 - foreach (var go in pool.Active) - KillObject(go); - pool.Active.Clear(); - // 销毁休眠实例 - while (pool.Available.Count > 0) - KillObject(pool.Available.Dequeue()); + foreach (var instance in pool.Slots.Values) + KillObject(instance); + pool.Slots.Clear(); } m_Pools.Clear(); + m_TotalCreated = 0; } private static void KillObject(GameObject go) @@ -76,31 +71,31 @@ namespace XericUI.XTable.Rendering.Elements #region 池操作 - /// 从池中获取实例(FIFO 复用;池空且未达上限则 Instantiate;已达上限返回 null) - public GameObject Acquire(GameObject prefab, int row, int col) + /// + /// 为指定 cellId 获取预制体实例。 + /// 若该 cellId 已有绑定的实例,直接返回;否则创建新实例(需未达上限)。 + /// + public GameObject Acquire(GameObject prefab, int cellId) { if (prefab == null) return null; var pool = GetOrCreatePool(prefab); - GameObject instance; - if (pool.Available.Count > 0) - { - instance = pool.Available.Dequeue(); - } - else if (m_TotalCreated < MAX_PREFAB_ITEMS) - { - instance = Object.Instantiate(prefab, m_Parent); - instance.hideFlags = POOL_FLAGS; - m_TotalCreated++; - } - else + // 已绑定 → 直接复用 + if (pool.Slots.TryGetValue(cellId, out var existing)) + return existing; + + // 创建新实例 + if (m_TotalCreated >= MAX_PREFAB_ITEMS) { Debug.LogWarning($"[XericUIActionTable] Prefab 池已达上限 {MAX_PREFAB_ITEMS},无法分配新实例"); return null; } - instance.name = $"_prefab_{row}_{col}"; + var instance = Object.Instantiate(prefab, m_Parent); + instance.hideFlags = POOL_FLAGS; + instance.name = $"_prefab_{cellId}"; + m_TotalCreated++; // 强制 RectTransform 左上角坐标系 RectTransform rt = instance.GetComponent(); @@ -109,21 +104,36 @@ namespace XericUI.XTable.Rendering.Elements rt.anchorMax = AnchorTopLeft; rt.pivot = PivotTopLeft; - pool.Active.Add(instance); + pool.Slots[cellId] = instance; return instance; } - /// 将指定实例归还对象池(不入池根,原地保留避免 OnEnable/OnDisable) + /// + /// 归还指定实例至槽位(仅从槽位移除 + 销毁对象,不在滚动时调用)。 + /// 预制体的回收仅发生在表格销毁时。 + /// public void Release(GameObject instance) { if (instance == null) return; - // 查找所属池 foreach (var kv in m_Pools) { - if (kv.Value.Active.Remove(instance)) + var pool = kv.Value; + // 查找并移除 + int? foundKey = null; + foreach (var slot in pool.Slots) { - kv.Value.Available.Enqueue(instance); + if (slot.Value == instance) + { + foundKey = slot.Key; + break; + } + } + if (foundKey.HasValue) + { + pool.Slots.Remove(foundKey.Value); + m_TotalCreated--; + KillObject(instance); return; } } @@ -132,18 +142,17 @@ namespace XericUI.XTable.Rendering.Elements KillObject(instance); } - /// 将所有活跃实例回收至池(原地保留,仅从活跃集移除并入队) + /// 将所有活跃槽位清空并销毁对象 public void ReturnAll() { foreach (var kv in m_Pools) { var pool = kv.Value; - foreach (var go in pool.Active) - { - pool.Available.Enqueue(go); - } - pool.Active.Clear(); + foreach (var instance in pool.Slots.Values) + KillObject(instance); + pool.Slots.Clear(); } + m_TotalCreated = 0; } /// 获取当前所有活跃实例(用于外部遍历回调) @@ -151,7 +160,7 @@ namespace XericUI.XTable.Rendering.Elements { if (prefab == null || !m_Pools.TryGetValue(prefab, out var pool)) return System.Array.Empty(); - return pool.Active; + return pool.Slots.Values; } #endregion diff --git a/Runtime/XTable/Rendering/Elements/SlotCounterPool.cs b/Runtime/XTable/Rendering/Elements/SlotCounterPool.cs new file mode 100644 index 0000000..0d86346 --- /dev/null +++ b/Runtime/XTable/Rendering/Elements/SlotCounterPool.cs @@ -0,0 +1,214 @@ +using System; +using System.Collections.Generic; + +using UnityEngine; + +namespace XericUI.XTable.Rendering.Elements +{ + /// + /// 基于计数器的槽位对象池(泛型)。 + /// 每个 cellId 映射到固定索引的 SlotCounter,槽位在计数器间通过 FrozenSlots 流转复用。 + /// 避免 Queue 模式下对象与位置解耦导致的堆叠问题,同时不触发 SetActive/SetParent 等生命周期事件。 + /// + /// 池对象类型 + public class SlotCounterPool where T : class + { + #region 内部类型 + + private class Slot + { + /// 当前绑定的单元格 ID(-1 = 空闲,可被偷取) + public int CellId = -1; + /// 池对象引用 + public T PooledObject; + } + + private class SlotCounter + { + public List Slots = new List(capacity: 4); + } + + #endregion + + #region 字段 + + /// 计数器数组大小(= 池上限,也是 cellId 取模的模数) + public const int POOL_SIZE = 256; + + private SlotCounter[] m_Counters = new SlotCounter[POOL_SIZE]; + + /// 冻结槽位列表——全局可偷取的休眠槽位(末尾偷取,避免 Canvas 重建) + private List m_FrozenSlots = new List(); + + /// 已创建对象总数(不含预制体 Instantiate) + private int m_CreatedCount; + + /// 对象工厂(构造函数传入) + private Func m_Factory; + + /// 已创建对象总数(只读) + public int CreatedCount => m_CreatedCount; + + #endregion + + #region 构造 + + public SlotCounterPool(Func factory) + { + m_Factory = factory ?? throw new ArgumentNullException(nameof(factory)); + } + + #endregion + + #region 池操作 + + /// + /// 为指定 cellId 获取一个池对象。 + /// 优先复用本计数器内的空闲槽位,其次从 FrozenSlots 偷取,最后创建新对象。 + /// + public T Acquire(int cellId) + { + int index = cellId % POOL_SIZE; + if (index < 0) index += POOL_SIZE; // 处理负 cellId + + var counter = m_Counters[index]; + if (counter == null) + { + counter = new SlotCounter(); + m_Counters[index] = counter; + } + + // 1. 在计数器内查找空闲槽位 + for (int i = 0; i < counter.Slots.Count; i++) + { + if (counter.Slots[i].CellId == -1) + { + counter.Slots[i].CellId = cellId; + return counter.Slots[i].PooledObject; + } + } + + // 2. 从 FrozenSlots 末尾偷取 + if (m_FrozenSlots.Count > 0) + { + int lastIdx = m_FrozenSlots.Count - 1; + var stolen = m_FrozenSlots[lastIdx]; + m_FrozenSlots.RemoveAt(lastIdx); + stolen.CellId = cellId; + counter.Slots.Add(stolen); + return stolen.PooledObject; + } + + // 3. 创建新对象 + if (m_CreatedCount < POOL_SIZE) + { + var obj = m_Factory(); + var slot = new Slot { CellId = cellId, PooledObject = obj }; + counter.Slots.Add(slot); + m_CreatedCount++; + return obj; + } + + // 4. 已达上限 + Debug.LogWarning($"[SlotCounterPool<{typeof(T).Name}>] 已达上限 {POOL_SIZE},无法分配新对象"); + return null; + } + + /// + /// 释放指定 cellId 占用的槽位,将其移入 FrozenSlots 供其他计数器复用。 + /// + public void Release(int cellId) + { + int index = cellId % POOL_SIZE; + if (index < 0) index += POOL_SIZE; + + var counter = m_Counters[index]; + if (counter == null) return; + + for (int i = 0; i < counter.Slots.Count; i++) + { + if (counter.Slots[i].CellId == cellId) + { + var slot = counter.Slots[i]; + counter.Slots.RemoveAt(i); + slot.CellId = -1; + m_FrozenSlots.Add(slot); + return; + } + } + } + + /// + /// 尝试通过池对象引用查找并释放对应槽位。 + /// 用于 Release(PooledObject) 场景,需要遍历查找。 + /// + public bool ReleaseByObject(T pooledObject) + { + if (pooledObject == null) return false; + + // 先遍历所有计数器的活跃槽位 + for (int ci = 0; ci < POOL_SIZE; ci++) + { + var counter = m_Counters[ci]; + if (counter == null) continue; + + for (int i = 0; i < counter.Slots.Count; i++) + { + if (ReferenceEquals(counter.Slots[i].PooledObject, pooledObject)) + { + var slot = counter.Slots[i]; + counter.Slots.RemoveAt(i); + slot.CellId = -1; + m_FrozenSlots.Add(slot); + return true; + } + } + } + return false; + } + + /// 将所有活跃槽位移入 FrozenSlots + public void ReturnAll() + { + for (int ci = 0; ci < POOL_SIZE; ci++) + { + var counter = m_Counters[ci]; + if (counter == null) continue; + + for (int i = counter.Slots.Count - 1; i >= 0; i--) + { + var slot = counter.Slots[i]; + counter.Slots.RemoveAt(i); + slot.CellId = -1; + m_FrozenSlots.Add(slot); + } + } + } + + /// + /// 彻底销毁所有池对象(包括计数器内活跃槽位和 FrozenSlots 中的冻结槽位)。 + /// + public void DestroyAll(Action destroyFunc) + { + if (destroyFunc == null) return; + + for (int ci = 0; ci < POOL_SIZE; ci++) + { + var counter = m_Counters[ci]; + if (counter == null) continue; + + foreach (var slot in counter.Slots) + destroyFunc(slot.PooledObject); + counter.Slots.Clear(); + } + + foreach (var slot in m_FrozenSlots) + destroyFunc(slot.PooledObject); + m_FrozenSlots.Clear(); + + m_CreatedCount = 0; + } + + #endregion + } +} diff --git a/Runtime/XTable/Rendering/Elements/SlotCounterPool.cs.meta b/Runtime/XTable/Rendering/Elements/SlotCounterPool.cs.meta new file mode 100644 index 0000000..8f4f390 --- /dev/null +++ b/Runtime/XTable/Rendering/Elements/SlotCounterPool.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 91facbccfc5ebff44b47c4b4fe51135b +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: