尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Unity RPG背包系统架构设计:从数据层到UI表现的全链路实现

Unity RPG背包系统架构设计:从数据层到UI表现的全链路实现 1. 项目缘起为什么RPG背包系统远不止一个UI列表做Unity3D RPG开发背包系统几乎是绕不开的一环。很多新手甚至一些有经验的开发者都容易把它想简单了不就是做个UI显示一堆图标点一下能使用或装备吗我最初也是这么想的直到自己真正上手去实现一个准备投入商业项目、需要承载复杂游戏逻辑的背包系统时才发现这里面的水有多深。一个健壮的RPG背包系统绝不仅仅是UI的排列组合。它本质上是一个数据管理、状态同步、逻辑分发和用户体验的综合体。你需要考虑成百上千件物品的数据如何高效存储和检索装备穿上后角色的属性如何实时、正确地更新消耗品使用、任务物品提交时如何触发一连串的游戏事件不同品质、不同类型的物品其交互逻辑如双击使用、右键菜单、拖拽合成如何优雅地设计以避免代码变成一坨难以维护的“意大利面条”更实际的问题是当你的背包UI在编辑器里跑得好好的一旦打包到移动端滑动列表卡顿了怎么办当策划临时要求增加一个“一键整理”功能或者“按品质筛选”时你的数据结构是否支持快速扩展这些都是在项目初期就必须深思熟虑的问题。本文将从一个实战者的角度抛开那些花哨的UI特效深入背包系统的“里子”分享一套经过项目验证、可扩展性强的实现方案涵盖从底层数据建模到上层UI交互的全链路。2. 核心架构设计数据层、逻辑层与表现层的分离在动手写第一行代码之前我们必须先确立一个清晰的架构。混乱的架构是后期无尽Bug和开发效率低下的根源。对于背包系统我强烈推荐采用“数据-逻辑-表现”三层分离的设计模式。这不是什么高深的理论而是为了让你在增加一个“装备强化”功能时不至于要去改UI的显示代码。2.1 数据层物品的“身份证”与“快照”数据层是系统的基石它只负责定义物品“是什么”和“当前状态如何”不关心“怎么显示”和“怎么用”。这里主要涉及两个核心类ItemData和ItemInstance。ItemData物品配置数据 你可以把它理解为物品的“身份证”或“蓝图”。它是只读的通常在游戏启动时从配置表如ScriptableObject、JSON或数据库加载。它定义了这类物品的固有属性。[CreateAssetMenu(fileName New Item, menuName Inventory/Item Data)] public class ItemData : ScriptableObject { public string itemId; // 唯一标识符如 “sword_001” public string itemName; public ItemType itemType; // 枚举Consumable, Equipment, Material, Quest... public ItemRarity rarity; // 枚举Common, Uncommon, Rare... public Sprite icon; public GameObject prefab; // 世界中的模型或UI中的详细预览模型 public int maxStack 1; // 最大堆叠数 [TextArea] public string description; // 不同类型物品的扩展属性使用继承或组合 public EquipmentAttributes equipmentAttributes; // 如果是装备包含攻击力、防御力等 public ConsumableEffect consumableEffect; // 如果是消耗品包含回复HP/MP的量 }注意使用ScriptableObject在编辑器内配置非常直观适合中小型项目。对于超大型项目或需要热更新的物品库可以考虑将核心配置放在JSON或数据库中用ScriptableObject作为运行时缓存或编辑器预览工具。ItemInstance物品实例数据 这是背包里某个具体格子中物品的“快照”。它引用一个ItemData并记录这个特定实例的动态信息。[System.Serializable] public class ItemInstance { public ItemData data; // 指向静态配置 public int currentStack; // 当前堆叠数量 public int slotIndex -1; // 所在背包格子索引-1表示未放入背包 // 动态属性例如装备的当前耐久度、附魔属性等 public int currentDurability; public ListEnchantment enchantments; // 创建一个新的实例深拷贝或浅拷贝根据需要 public ItemInstance(ItemData sourceData, int stack 1) { data sourceData; currentStack Mathf.Min(stack, sourceData.maxStack); currentDurability (sourceData.equipmentAttributes ! null) ? sourceData.equipmentAttributes.maxDurability : 0; } public bool IsStackableWith(ItemInstance other) { return other ! null data ! null other.data ! null data.itemId other.data.itemId currentStack data.maxStack; } }这种分离的好处是巨大的。ItemData一份配置可以被成千上万个ItemInstance引用极大节省内存。所有逻辑判断如能否堆叠、是否同类都基于itemId清晰可靠。2.2 逻辑层背包管理器与游戏服务的桥梁逻辑层是系统的大脑核心是一个单例类InventoryManager。它不直接操作UI而是维护一个ItemInstance的集合如ListItemInstance或Dictionaryint, ItemInstance并对外提供所有核心操作方法。public class InventoryManager : MonoBehaviour { public static InventoryManager Instance { get; private set; } // 核心数据容器 private ListItemInstance items new ListItemInstance(); public int Capacity { get; private set; } 30; // 背包容量 // 事件用于通知UI更新 public event Action OnInventoryChanged; public event ActionItemInstance OnItemUsed; public event ActionItemInstance, int OnItemEquipped; // 物品装备槽位 void Awake() { Instance this; } // 核心方法添加物品 public bool AddItem(ItemData itemData, int amount 1) { // 1. 处理可堆叠物品寻找已有堆叠 if (itemData.maxStack 1) { foreach (var instance in items) { if (instance.IsStackableWith(new ItemInstance(itemData))) { int spaceLeft instance.data.maxStack - instance.currentStack; int addToThisStack Mathf.Min(spaceLeft, amount); instance.currentStack addToThisStack; amount - addToThisStack; OnInventoryChanged?.Invoke(); if (amount 0) return true; } } } // 2. 处理剩余物品占用新格子 while (amount 0 items.Count Capacity) { int stackSize Mathf.Min(amount, itemData.maxStack); items.Add(new ItemInstance(itemData, stackSize)); amount - stackSize; } OnInventoryChanged?.Invoke(); return amount 0; // 返回是否全部添加成功 } // 使用物品 public void UseItem(int slotIndex) { if (slotIndex 0 || slotIndex items.Count) return; ItemInstance instance items[slotIndex]; if (instance.data.itemType ItemType.Consumable) { // 调用游戏服务如PlayerStats.Instance.Heal(instance.data.consumableEffect.healAmount); OnItemUsed?.Invoke(instance); instance.currentStack--; if (instance.currentStack 0) { items.RemoveAt(slotIndex); } OnInventoryChanged?.Invoke(); } else if (instance.data.itemType ItemType.Equipment) { // 触发装备逻辑 OnItemEquipped?.Invvoke(instance, GetEquipmentSlot(instance.data)); } } // 其他方法RemoveItem, SwapItems, FindItemById, etc. }逻辑层的关键在于“纯洁性”。它只负责物品数据的增删改查和核心规则如堆叠、容量检查。具体的视觉效果如图标飞入动画、声音播放都应该通过监听OnInventoryChanged等事件在表现层去实现。2.3 表现层UI是数据的“仆人”表现层也就是我们的UI部分如InventoryUI、SlotUI其职责只有一个忠实地反映逻辑层数据的状态并将用户的操作意图传递给逻辑层。它不应该存储任何核心业务逻辑。SlotUI格子UI组件public class SlotUI : MonoBehaviour, IPointerClickHandler, IBeginDragHandler, IDragHandler, IEndDragHandler { public Image iconImage; public Text amountText; public GameObject highlight; private int slotIndex; private ItemInstance currentItem; public void Setup(int index) { slotIndex index; ClearSlot(); } public void UpdateSlot(ItemInstance item) { currentItem item; if (item ! null item.data ! null) { iconImage.sprite item.data.icon; iconImage.enabled true; amountText.text item.currentStack 1 ? item.currentStack.ToString() : ; // 可以根据item.rarity设置边框颜色等 } else { ClearSlot(); } } private void ClearSlot() { iconImage.sprite null; iconImage.enabled false; amountText.text ; highlight.SetActive(false); } // 将UI事件转化为逻辑调用 public void OnPointerClick(PointerEventData eventData) { if (eventData.button PointerEventData.InputButton.Right) { // 右键使用 InventoryManager.Instance.UseItem(slotIndex); } } // 拖拽相关逻辑... }InventoryUI背包面板控制器public class InventoryUI : MonoBehaviour { public GameObject slotPrefab; public Transform slotContainer; private ListSlotUI slots new ListSlotUI(); void Start() { InitializeSlots(); InventoryManager.Instance.OnInventoryChanged RefreshUI; RefreshUI(); // 初始刷新 } void InitializeSlots() { for (int i 0; i InventoryManager.Instance.Capacity; i) { GameObject slotObj Instantiate(slotPrefab, slotContainer); SlotUI slot slotObj.GetComponentSlotUI(); slot.Setup(i); slots.Add(slot); } } void RefreshUI() { // 获取逻辑层当前所有物品 var allItems InventoryManager.Instance.GetAllItems(); for (int i 0; i slots.Count; i) { // 将逻辑层的索引与UI格子一一对应更新 ItemInstance item (i allItems.Count) ? allItems[i] : null; slots[i].UpdateSlot(item); } } }通过这种架构当逻辑层数据变化时UI自动更新。策划想调整背包容量只需改InventoryManager.Capacity并重启UI即可。想增加一个“仓库”系统完全可以复用这套架构只需创建另一个InventoryManager实例和对应的WarehouseUI。3. 关键功能实现详解从拖拽交换到数据持久化有了稳固的架构我们就可以在上面搭建丰富的功能。这些功能是玩家能直接感知到的也是开发中容易踩坑的地方。3.1 物品拖拽与交换处理多端交互的细节拖拽是背包系统最基础的交互但实现一个手感流畅、逻辑正确的拖拽需要处理好整个生命周期。实现步骤与核心代码开始拖拽IBeginDragHandler 记录被拖拽的物品信息并创建一个跟随鼠标的“拖拽物”视觉反馈。// 在SlotUI中 private static GameObject draggingItem; // 静态变量保证全局只有一个拖拽物 private static SlotUI dragStartSlot; public void OnBeginDrag(PointerEventData eventData) { if (currentItem null) return; dragStartSlot this; draggingItem new GameObject(Dragging Icon); draggingItem.transform.SetParent(GetTopLevelCanvas().transform); // 置于顶层Canvas draggingItem.transform.SetAsLastSibling(); Image dragImage draggingItem.AddComponentImage(); dragImage.sprite iconImage.sprite; dragImage.raycastTarget false; // 关键避免拖拽物挡住射线检测 dragImage.SetNativeSize(); // 立即隐藏原格子的图标避免“分身” iconImage.enabled false; }拖拽中IDragHandler 更新“拖拽物”的位置。public void OnDrag(PointerEventData eventData) { if (draggingItem ! null) { RectTransformUtility.ScreenPointToLocalPointInRectangle( GetTopLevelCanvas().transform as RectTransform, eventData.position, eventData.pressEventCamera, out Vector2 localPos); draggingItem.transform.localPosition localPos; } }结束拖拽IEndDragHandler 这是逻辑的核心。需要判断鼠标释放在了哪个UI元素上。public void OnEndDrag(PointerEventData eventData) { if (draggingItem null) return; Destroy(draggingItem); draggingItem null; // 恢复原格子图标如果物品没被移走 if (dragStartSlot.currentItem ! null) { dragStartSlot.iconImage.enabled true; } // 射线检测释放点 ListRaycastResult results new ListRaycastResult(); EventSystem.current.RaycastAll(eventData, results); SlotUI dropSlot null; foreach (var result in results) { dropSlot result.gameObject.GetComponentSlotUI(); if (dropSlot ! null) break; // 找到第一个有效的SlotUI } // 执行交换或移动逻辑 if (dropSlot ! null dropSlot ! dragStartSlot) { InventoryManager.Instance.SwapItems(dragStartSlot.slotIndex, dropSlot.slotIndex); } // 如果释放到非SlotUI区域可以设计为丢弃物品弹出确认框 else if (/* 检测到特定丢弃区域 */) { // InventoryManager.Instance.RemoveItem(dragStartSlot.slotIndex); } dragStartSlot null; }踩坑记录这里最大的坑是raycastTarget。拖拽物的Image必须设为false否则它会挡住后续的射线检测导致你永远无法检测到下方的SlotUI。另外EventSystem.current.RaycastAll在UI层级复杂时可能性能不佳对于大型背包可以考虑使用GraphicRaycaster的特定层或物理射线针对3D背包进行优化。InventoryManager.SwapItems的实现public void SwapItems(int indexA, int indexB) { if (indexA 0 || indexA items.Count || indexB 0 || indexB items.Count) return; // 如果是同一个格子或者目标格子为空直接移动 if (indexA indexB) return; ItemInstance itemA items[indexA]; ItemInstance itemB items[indexB]; // 情况1目标格子为空直接移动 if (itemB null) { items[indexB] itemA; items[indexA] null; if (itemA ! null) itemA.slotIndex indexB; } // 情况2两者为同一可堆叠物品尝试合并 else if (itemA ! null itemB.IsStackableWith(itemA)) { int transferAmount Mathf.Min(itemA.currentStack, itemB.data.maxStack - itemB.currentStack); itemB.currentStack transferAmount; itemA.currentStack - transferAmount; if (itemA.currentStack 0) { items[indexA] null; } } // 情况3交换位置 else { items[indexA] itemB; items[indexB] itemA; if (itemA ! null) itemA.slotIndex indexB; if (itemB ! null) itemB.slotIndex indexA; } OnInventoryChanged?.Invoke(); }3.2 数据持久化如何让玩家的心血不丢失背包数据是玩家的核心资产持久化必须可靠。Unity提供了PlayerPrefs、JSON/XML序列化、以及BinaryFormatter或第三方序列化库如Newtonsoft.Json、MessagePack等多种方式。为什么我不推荐PlayerPrefs 它本质上是用来存简单设置的如音量大小数据以明文形式存储在注册表或plist文件中容易被玩家修改且存储大量结构化数据非常笨拙。推荐方案JSON 加密。 平衡了可读性便于调试、性能和安全性。定义可序列化的数据类 我们需要一个轻量的、只包含必要数据的类来保存。[System.Serializable] public class SaveData { public ListSavedItem savedItems new ListSavedItem(); public int gold; // ... 其他需要保存的数据如任务进度 } [System.Serializable] public class SavedItem { public string itemId; // 只存ID不存整个ItemData引用 public int stackCount; public int slotIndex; public int durability; // 动态属性 // 注意这里不要保存对ScriptableObject的直接引用 }保存过程public void SaveInventory() { SaveData saveData new SaveData(); foreach (var instance in items) { if (instance ! null instance.data ! null) { saveData.savedItems.Add(new SavedItem { itemId instance.data.itemId, stackCount instance.currentStack, slotIndex instance.slotIndex, durability instance.currentDurability }); } } string json JsonUtility.ToJson(saveData, true); // 美观格式便于调试 // 简单加密对字符串进行XOR或Base64编码非绝对安全但增加修改门槛 byte[] bytes System.Text.Encoding.UTF8.GetBytes(json); // 这里可以加入简单的加密算法例如每个字节与一个密钥异或 for (int i 0; i bytes.Length; i) { bytes[i] ^ 0x55; } string encryptedData System.Convert.ToBase64String(bytes); System.IO.File.WriteAllText(GetSavePath(), encryptedData); Debug.Log(背包数据已保存。); } private string GetSavePath() { return Path.Combine(Application.persistentDataPath, inventory.sav); }加载过程public void LoadInventory() { string path GetSavePath(); if (!File.Exists(path)) { Debug.Log(无存档文件初始化空背包。); items.Clear(); OnInventoryChanged?.Invoke(); return; } string encryptedData File.ReadAllText(path); byte[] bytes System.Convert.FromBase64String(encryptedData); // 解密 for (int i 0; i bytes.Length; i) { bytes[i] ^ 0x55; } string json System.Text.Encoding.UTF8.GetString(bytes); SaveData saveData JsonUtility.FromJsonSaveData(json); // 清空当前背包 items.Clear(); // 根据Capacity初始化空位 for (int i 0; i Capacity; i) { items.Add(null); } // 需要一个通过itemId获取ItemData的中央仓库 ItemDataManager dataManager ItemDataManager.Instance; foreach (var savedItem in saveData.savedItems) { ItemData data dataManager.GetItemDataById(savedItem.itemId); if (data ! null savedItem.slotIndex Capacity) { ItemInstance instance new ItemInstance(data, savedItem.stackCount); instance.currentDurability savedItem.durability; instance.slotIndex savedItem.slotIndex; items[savedItem.slotIndex] instance; } } OnInventoryChanged?.Invoke(); Debug.Log(背包数据已加载。); }关键点 这里需要一个ItemDataManager单例它在游戏启动时加载所有ItemData例如从Resources文件夹或AssetBundle并提供GetItemDataById方法。这样存档里只存轻量的ID和数值运行时再通过ID还原出完整的物品对象保证了存档的轻量和数据的一致性。3.3 性能优化应对成百上千的物品列表当背包容量很大比如200格子且物品繁多时直接为每个格子实例化一个GameObject在移动设备上滚动时可能会卡顿。这时就需要用到UI循环列表。核心思想 只创建可视区域内的那几个格子GameObject。当滚动时循环复用这些格子只是更新它们显示的数据。这可以极大减少Draw Call和GC垃圾回收压力。实现方案手动实现 监听滚动条的onValueChanged事件计算当前应显示的数据索引范围然后更新复用池中每个格子的内容和位置。逻辑较为复杂。使用成熟插件 这是更高效的选择。社区有大量优秀的免费或付费插件例如Unity UI Extensions免费 包含一个简单的循环列表。EnhancedScroller/SuperScrollViewAsset Store 专门为Unity UI设计的强大循环列表组件文档完善功能丰富。以使用一个典型循环列表组件为例你的InventoryUI会发生变化public class InventoryUI : MonoBehaviour { // 不再需要slotContainer和大量SlotUI实例 public EnhancedScroller scroller; public EnhancedScrollerCellView cellViewPrefab; // 一个包含若干SlotUI的单元格例如一行5个 void Start() { scroller.cellViewInstantiated HandleCellViewInstantiated; scroller.cellViewWillRecycle HandleCellViewWillRecycle; scroller.Delegate this; // 需要实现IEnhancedScrollerDelegate InventoryManager.Instance.OnInventoryChanged RefreshData; } void RefreshData() { // 告诉Scroller数据总数发生了变化 scroller.ReloadData(); } // 实现接口方法告诉Scroller有多少个“行” public int GetNumberOfCells(EnhancedScroller scroller) { int totalItems InventoryManager.Instance.GetItemCount(); int itemsPerRow 5; return Mathf.CeilToInt((float)totalItems / itemsPerRow); } // 告诉Scroller每一行的高度 public float GetCellViewSize(EnhancedScroller scroller, int dataIndex) { return 120f; // 你的格子高度 } // 为某一行获取或创建CellView并设置数据 public EnhancedScrollerCellView GetCellView(EnhancedScroller scroller, int dataIndex, int cellIndex) { CellView cellView scroller.GetCellView(cellViewPrefab) as CellView; // 计算这行应该显示哪5个物品的索引 int startIndex dataIndex * 5; ItemInstance[] rowData new ItemInstance[5]; for (int i 0; i 5; i) { int globalIndex startIndex i; rowData[i] InventoryManager.Instance.GetItemAt(globalIndex); } cellView.SetData(dataIndex, rowData); return cellView; } }这样无论背包逻辑上有多少物品屏幕上实际存在的GameObject可能只有10-20行50-100个格子性能得到根本性改善。4. 高级功能与扩展思路让背包系统真正“活”起来基础功能实现后背包系统可以成为一个强大的游戏机制驱动核心而不仅仅是一个储物箱。4.1 物品分类、筛选与搜索当物品超过50件时让玩家手动寻找会非常痛苦。分类、筛选和搜索是必备的体验优化功能。实现原理 在InventoryManager中维护当前物品列表的一个“视图”View而不是直接操作原始数据。所有筛选操作都是基于原始数据生成一个新的视图列表然后通知UI刷新这个视图。public class InventoryManager : MonoBehaviour { private ListItemInstance allItems new ListItemInstance(); // 主数据 private ListItemInstance filteredView new ListItemInstance(); // 当前视图 private ItemType currentFilter ItemType.All; private string currentSearchString ; public void ApplyFilter(ItemType filterType) { currentFilter filterType; UpdateFilteredView(); OnInventoryViewChanged?.Invoke(); // 新事件通知UI视图变了 } public void ApplySearch(string searchText) { currentSearchString searchText.ToLower(); UpdateFilteredView(); OnInventoryViewChanged?.Invoke(); } private void UpdateFilteredView() { filteredView.Clear(); foreach (var item in allItems) { if (item null) continue; bool typeMatch (currentFilter ItemType.All) || (item.data.itemType currentFilter); bool nameMatch string.IsNullOrEmpty(currentSearchString) || item.data.itemName.ToLower().Contains(currentSearchString); if (typeMatch nameMatch) { filteredView.Add(item); } } // 可以在这里对filteredView进行排序如按品质、按等级 } public ListItemInstance GetCurrentViewItems() filteredView; }UI层尤其是循环列表现在绑定的是GetCurrentViewItems()而不是GetAllItems()。当玩家点击“武器”筛选按钮或输入搜索文字时调用ApplyFilter或ApplySearch视图更新UI自动刷新展示的就是筛选后的结果。4.2 与装备系统、任务系统、商店系统的联动背包不应是孤岛。物品的流动构成了游戏的经济循环。与装备系统联动 当InventoryManager的OnItemEquipped事件触发时EquipmentManager会监听这个事件。它负责检查装备槽位是否空闲、计算属性加成、更换角色模型并可能将替换下来的旧装备送回背包触发AddItem。// EquipmentManager.cs void OnEnable() { InventoryManager.Instance.OnItemEquipped HandleEquipItem; } void HandleEquipItem(ItemInstance item, int slot) { ItemInstance oldEquip currentEquipment[slot]; if (oldEquip ! null) { // 卸下旧装备加回背包 InventoryManager.Instance.AddItem(oldEquip.data, oldEquip.currentStack); // 从角色属性中减去旧装备加成 playerStats.RemoveModifiers(oldEquip.data.equipmentAttributes); } // 穿上新装备 currentEquipment[slot] item; // 将加成应用到角色属性 playerStats.ApplyModifiers(item.data.equipmentAttributes); // 更新角色外观Instantiate item.data.prefab }与任务系统联动 任务系统会监听背包的变动。例如一个“收集10个狼牙”的任务会在玩家每次获得“狼牙”物品时检查OnInventoryChanged事件并更新任务进度。// QuestManager.cs void OnInventoryChangedListener() { foreach (var quest in activeQuests) { foreach (var objective in quest.collectObjectives) { // 检查背包中该物品的数量 int count InventoryManager.Instance.GetItemCountById(objective.requiredItemId); objective.currentAmount count; if (objective.IsCompleted()) { // 标记目标完成 } } } }与商店系统联动 商店本质上是一个有特定物品列表、且交易规则不同的“背包”。买卖操作的核心仍然是调用InventoryManager的AddItem获得金钱或物品和RemoveItem扣除金钱或物品只是中间经过了价格计算。可以创建一个ShopInventory类继承或组合InventoryManager增加BuyItem和SellItem方法在其中处理货币校验。4.3 网络游戏中的背包同步对于多人游戏如MMORPG背包数据必须存储在服务器端。客户端的InventoryManager不再拥有最终决定权而是作为服务器数据的缓存和表现。通信流程简化示例客户端请求使用物品C_UseItem。服务器验证物品是否存在、冷却时间、距离等。服务器执行逻辑扣除物品、应用效果、更新数据库。服务器广播结果S_InventoryUpdate给所有相关客户端包含变更后的背包数据快照或增量更新。客户端收到指令后更新本地的InventoryManager数据并触发OnInventoryChangedUI随之更新。关键点 客户端要有良好的状态预测和纠错机制。例如使用物品时客户端可以先立即在本地显示效果如血条回复让操作有即时反馈。如果服务器后来拒绝了该操作客户端再回滚状态并给出提示“物品使用失败”。这需要更复杂的状态同步设计但能极大提升操作手感。5. 实战避坑指南与性能调优最后分享一些从真实项目踩坑中总结出的经验这些在官方文档里很少提及。坑1ScriptableObject引用在存档中的陷阱前面提到存档不要存ScriptableObject引用只存itemId。这是因为ScriptableObject是资产文件。如果你通过AssetBundle更新了游戏修改了ItemData的配置但玩家存档里还存着旧的资产引用可能会引发空引用或数据错乱。通过ID加载总能获取到最新的配置数据。坑2频繁的Instantiate/Destroy导致GC拖拽时创建/销毁临时图标、刷新UI时大量修改Text组件文本都会产生垃圾。对于拖拽图标可以使用对象池ObjectPool。对于数量文本可以检查值是否真的改变了再赋值避免不必要的UI重建。// 在SlotUI.UpdateSlot中 if (item ! null) { int newAmount item.currentStack; if (newAmount 1) { if (amountText.text ! newAmount.ToString()) amountText.text newAmount.ToString(); } else { if (!string.IsNullOrEmpty(amountText.text)) amountText.text ; } }坑3排序算法的性能“一键整理”功能需要对物品列表进行排序。如果物品很多1000一个低效的排序算法会卡顿。可以使用C#ListT.Sort方法并传递自定义的比较器它的底层是快速排序效率较高。排序规则应尽量简单先按类型再按品质最后按ID。坑4移动端输入处理在手机上长按通常作为替代右键的交互方式显示详情或菜单。需要同时处理好IPointerDownHandler、IPointerUpHandler和IPointerClickHandler并设置一个计时器来区分点击和长按。注意在长按触发后要阻止后续的点击事件防止一个操作触发两个行为。性能调优建议使用Sprite Atlas 将所有物品图标打包到一个图集中这是一个减少Draw Call最有效的方法。避免在Update中频繁查找 像Find、GetComponent这类函数比较耗时。在Start或Awake中缓存引用。对UI进行动静分离 将频繁变化的元素如数量文本、冷却倒计时和静态元素如背景框放在不同的Canvas下。因为Unity UI中一个Canvas下的任一元素变化都会导致整个Canvas重建网格Rebuild。分离后静态Canvas不需要重绘提升了效率。使用Profiler Unity的Profiler是你的最佳朋友。在编辑器里运行游戏打开Profiler重点观察CPU Usage中的UI部分和GC Alloc。它能直观地告诉你性能瓶颈在哪里。
返回列表