尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

Unity UGUI背包系统开发全解析:从架构设计到性能优化

Unity UGUI背包系统开发全解析:从架构设计到性能优化 1. 项目概述为什么需要一个好的背包系统在Unity里做游戏尤其是RPG、生存建造或者任何有收集元素的游戏背包系统几乎是绕不开的核心功能。它不仅仅是界面上那几个格子背后牵扯到UI交互、数据管理、逻辑解耦和性能优化等一系列问题。很多新手开发者包括几年前的我一开始可能会觉得背包很简单不就是一堆图片格子点一下放东西进去吗但真做起来才发现坑一个接一个——物品拖拽卡顿、数据保存混乱、UI更新不及时、扩展性差到想重写。UGUI作为Unity官方的UI解决方案功能强大但细节繁多。一个健壮的UGUI背包系统需要将视图UI显示、数据物品属性、逻辑使用、交换、合成清晰地分离。这不仅仅是让代码更好维护更是为了应对策划频繁的需求变更“能不能加个自动整理”“物品叠加规则要改一下”“我们需要支持背包分页和标签过滤”…… 如果没有一个良好的架构这些需求会成为灾难。所以这篇内容我会从一个实战开发者的角度彻底拆解一个基于UGUI的背包系统。我会从最基础的UI搭建讲起深入到数据驱动、事件通信、高级交互如拖拽、工具提示、自动整理最后聊聊性能优化和扩展思路。目标不只是让你“做出来”而是让你理解每一步背后的“为什么”以及如何构建一个经得起折腾的、可用于实际项目的背包系统。2. 背包系统的核心架构设计在动手写代码之前花点时间思考架构是绝对值得的。一个混乱的背包代码会像一团乱麻后期添加任何功能都举步维艰。我推崇的是MVCModel-View-Controller或其变体如MVVM的架构思想但在Unity UGUI的语境下我们可以将其具体化为更贴合引擎的模式。2.1 数据层物品的“灵魂”数据层是背包的基石它不关心UI怎么画只关心“物品是什么”。这里我们需要设计一个核心的Item数据类。[System.Serializable] public class ItemData { public string ItemID; // 物品唯一标识符 public string ItemName; // 物品名称 public string Description; // 物品描述 public Sprite Icon; // 物品图标 public int MaxStackCount 1; // 最大堆叠数量 public ItemType Type; // 物品类型如消耗品、材料、装备 public Dictionarystring, int Properties; // 扩展属性如攻击力、耐久度 // 深拷贝方法用于物品拆分、交易时创建新实例 public ItemData DeepCopy() { ItemData newData new ItemData(); newData.ItemID this.ItemID; newData.ItemName this.ItemName; // ... 复制其他字段 newData.Properties new Dictionarystring, int(this.Properties); return newData; } } public enum ItemType { Consumable, Material, Equipment, Quest }为什么这样设计ItemID用字符串或GUID便于通过配置表如Excel、ScriptableObject动态加载物品信息。比使用枚举更灵活支持热更新。MaxStackCount将堆叠逻辑内聚在数据层UI层只需根据这个值显示数量。Properties字典这是一个关键技巧。不同物品属性差异很大武器有攻击力药水有恢复量。使用字典可以避免为每种物品创建庞大的子类通过键值对存储任意属性非常灵活。当然对于装备这类复杂物品可能仍需继承自一个EquipmentItemData类。深拷贝当从一个堆叠物品中拆分出部分时你需要创建一份新的数据实例避免引用同一对象导致数据错乱。2.2 视图层物品的“皮囊”视图层负责将ItemData可视化。核心是ItemSlot物品槽和ItemUI物品显示单元。public class ItemSlot : MonoBehaviour, IPointerEnterHandler, IPointerExitHandler, IDropHandler { public Image backgroundImage; // 槽位背景 public ItemUI currentItemUI; // 当前槽位存放的物品UI private int slotIndex; // 槽位索引用于关联数据 // 设置槽位索引和初始状态 public void Initialize(int index) { slotIndex index; backgroundImage.color Color.white; // 默认颜色 } // 当物品被拖拽到此槽位上方时触发 public void OnPointerEnter(PointerEventData eventData) { // 高亮显示提示可放置 if (DragHandler.Instance.CurrentlyDraggedItem ! null) { backgroundImage.color Color.yellow; } } public void OnPointerExit(PointerEventData eventData) { backgroundImage.color Color.white; } // 当物品在此槽位放下时触发 public void OnDrop(PointerEventData eventData) { // 通知控制器处理放置逻辑 InventoryController.Instance.HandleItemDrop(this, DragHandler.Instance.CurrentlyDraggedItem); } }视图层的职责分离ItemSlot是一个“容器”或“位置”。它知道自己的索引处理与这个位置相关的UI事件悬停、放下。它不直接持有ItemData只持有ItemUI的引用。ItemUI是ItemData的可视化代表。它包含Icon、数量Text等组件并负责根据传入的ItemData更新自己的显示。public class ItemUI : MonoBehaviour { public Image iconImage; public Text countText; private ItemData itemData; public void Setup(ItemData data) { itemData data; iconImage.sprite data.Icon; iconImage.enabled true; // 确保显示 countText.text data.MaxStackCount 1 ? data.CurrentStackCount.ToString() : ; // 可以根据ItemType设置不同的边框颜色等 } public void Clear() { itemData null; iconImage.sprite null; iconImage.enabled false; countText.text ; } }这种分离使得逻辑非常清晰数据变更时控制器通知ItemUI更新视图UI交互时ItemSlot将事件抛给控制器处理。2.3 控制层系统的“大脑”控制层是视图和数据的桥梁也是所有业务逻辑的汇聚点。我们通常会有一个InventoryManager或InventoryController的单例类注意合理使用单例避免过度耦合。它的核心职责包括管理背包数据模型维护一个ListItemData或ItemData[]来表示背包内物品的数据状态。响应UI事件接收来自ItemSlot的拖拽放下、点击使用等事件并执行相应的逻辑如交换物品、叠加物品。更新视图当背包数据发生变化添加、删除、移动物品时负责找到对应的ItemSlot和ItemUI并调用其更新方法。提供对外接口例如AddItem(ItemData item),RemoveItem(string itemID, int count),SwapItems(int indexA, int indexB)等供其他系统商店、合成台调用。关键设计使用事件进行解耦不要让控制器直接持有所有ItemSlot和ItemUI的引用并直接操作它们。这会导致代码僵化。推荐使用C#的事件event或UnityEvent或者一个轻量级的消息中心。例如当数据层物品发生变化时public class InventoryController : MonoBehaviour { public static InventoryController Instance; public ListItemData items new ListItemData(); // 背包数据 // 定义一个事件当背包数据改变时触发 public event Action OnInventoryUpdated; public bool AddItem(ItemData itemToAdd) { // ... 尝试堆叠或寻找空位 ... bool success // ... 添加逻辑; if (success) { OnInventoryUpdated?.Invoke(); // 通知所有监听者 } return success; } }然后在ItemSlot或一个专门的InventoryUI类中监听这个事件void Start() { InventoryController.Instance.OnInventoryUpdated RefreshSlotUI; } void RefreshSlotUI() { // 根据自己对应的索引从InventoryController.Instance.items中获取数据并更新ItemUI }这种方式实现了数据与UI的完全解耦控制器不关心有多少个槽位UI也不关心数据如何变化它们只通过事件通信。3. UGUI界面搭建与自动布局有了架构蓝图我们开始动手搭建UI。UGUI的自动布局组件能让我们事半功倍。3.1 Canvas与基础设置首先在场景中创建一个Canvas命名为InventoryCanvas。关键设置如下Canvas Scaler这是确保UI在不同分辨率下正常显示的核心。UI Scale Mode: 设置为Scale With Screen Size。这是最常用的模式。Reference Resolution: 设定一个设计分辨率例如 1920 x 1080。这意味着UI是在这个分辨率下设计的。Screen Match Mode: 通常选择Match Width or Height并设置Match值为 0.5在宽度和高度之间平衡。这能保证UI在宽屏和竖屏下都有较好的自适应效果。Graphic Raycaster默认已添加用于处理UI的点击、拖拽等输入事件。注意对于复杂的背包UI可以考虑使用多个Canvas。例如将背包主界面、工具提示Tooltip、拖拽中的物品图标分别放在不同的Canvas上并设置不同的Sort Order。这样可以避免因一个Canvas下的元素重绘而影响其他部分是优化UI渲染性能的常用手段。3.2 使用Grid Layout Group构建背包格子背包的核心是一排排整齐的格子。手动排列它们既低效又难以维护。Grid Layout Group组件是我们的首选。在Canvas下创建一个Panel命名为InventoryGrid。为InventoryGrid添加Grid Layout Group组件。Cell Size: 设置每个格子的尺寸例如 100 x 100。Spacing: 格子之间的间隔例如 5 x 5。Start Corner/Start Axis: 定义布局起始角落和方向水平优先或垂直优先。Constraint: 可以固定行数或列数或者让其自由流动。对于有固定格子数的背包选择Fixed Column Count或Fixed Row Count很方便。创建格子预制体ItemSlotPrefab它是一个Image作为背景用于显示槽位状态空、高亮。挂载我们之前编写的ItemSlot脚本。在它的子节点下创建一个稍小的Image作为ItemIcon用于显示物品图标和一个Text作为CountText用于显示堆叠数量。这个结构体就是ItemUI的视觉部分。在InventoryGrid下实例化多个ItemSlotPrefab。Grid Layout Group会自动将它们排列整齐。实操心得在编辑模式下你可以通过修改Grid Layout Group的参数实时看到布局变化非常方便。如果背包格子非常多比如超过100个直接实例化所有格子可能会影响初始化性能。可以考虑使用UI虚拟化技术即只创建可视区域内的格子随着滚动动态回收和创建。这通常需要配合Scroll Rect和自定义脚本来实现是高级背包系统的优化点。3.3 物品图标的拖拽功能实现拖拽是背包系统最直观的交互。UGUI通过事件系统提供了IBeginDragHandler,IDragHandler,IEndDragHandler接口。但直接在每个ItemUI上实现这些接口会导致代码分散。更好的做法是使用一个全局的DragHandler。创建拖拽代理 创建一个始终位于屏幕顶层的DragIcon一个独立的Image游戏对象放在高Sort Order的Canvas下。初始状态为禁用。public class DragHandler : MonoBehaviour { public static DragHandler Instance; public Image DragIconImage; public ItemData DraggingItemData; public ItemSlot SourceSlot; // 物品来源的槽位 void Awake() { Instance this; } public void StartDrag(ItemData data, Sprite icon, ItemSlot source) { DraggingItemData data; SourceSlot source; DragIconImage.sprite icon; DragIconImage.rectTransform.sizeDelta new Vector2(100, 100); // 设置拖拽图标大小 DragIconImage.gameObject.SetActive(true); } public void UpdateDragPosition(Vector2 position) { DragIconImage.rectTransform.position position; } public void EndDrag() { DragIconImage.gameObject.SetActive(false); DraggingItemData null; SourceSlot null; } }在ItemUI上触发拖拽 在ItemUI脚本中监听OnBeginDrag事件。public class ItemUI : MonoBehaviour, IBeginDragHandler, IDragHandler, IEndDragHandler { // ... 其他代码 ... public void OnBeginDrag(PointerEventData eventData) { if (itemData null) return; // 通知全局拖拽管理器开始拖拽 DragHandler.Instance.StartDrag(itemData, iconImage.sprite, GetComponentInParentItemSlot()); // 可以设置当前ItemUI为半透明表示已被“拿起” iconImage.color new Color(1,1,1,0.5f); } public void OnDrag(PointerEventData eventData) { // 更新拖拽图标的位置 DragHandler.Instance.UpdateDragPosition(eventData.position); } public void OnEndDrag(PointerEventData eventData) { DragHandler.Instance.EndDrag(); iconImage.color Color.white; // 恢复颜色 // 注意物品的实际放置逻辑在 ItemSlot 的 OnDrop 中处理 } }在ItemSlot上完成放置 如前所述ItemSlot实现了IDropHandler。在OnDrop方法中它从DragHandler.Instance获取正在拖拽的物品信息和来源槽位并调用控制器的逻辑处理方法。这个模式的优势将拖拽的视觉表现跟随鼠标的图标和逻辑处理数据交换分离。逻辑全部集中在控制器中易于管理和调试。4. 核心逻辑的深度实现UI搭建好后最复杂的部分来了——处理物品移动、叠加、交换的所有规则。4.1 物品放置与交换算法这是背包系统的核心算法。在InventoryController.HandleItemDrop(ItemSlot targetSlot, ItemData draggedItem)方法中我们需要处理多种情况目标槽位为空最简单直接将物品数据移动到目标槽位索引清空来源槽位。目标槽位有物品且物品ID相同可堆叠计算目标槽位剩余可堆叠空间int spaceLeft targetItem.MaxStackCount - targetItem.CurrentStackCount。如果拖拽物品数量 spaceLeft则全部合并到目标槽位清空来源槽位。如果拖拽物品数量 spaceLeft则将目标槽位堆叠至满剩余的放回来源槽位或继续留在拖拽中取决于交互设计。目标槽位有物品且物品ID不同交换两个槽位的物品数据。从外部如场景中拾取添加到背包需要遍历背包数据先寻找可堆叠的槽位再寻找空槽位。public void HandleItemDrop(ItemSlot targetSlot, ItemSlot sourceSlot) { if (targetSlot null || sourceSlot null || targetSlot sourceSlot) return; ItemData targetData items[targetSlot.Index]; ItemData sourceData items[sourceSlot.Index]; // 情况1: 目标槽位为空 if (targetData null) { items[targetSlot.Index] sourceData; items[sourceSlot.Index] null; } // 情况2: 物品相同且可堆叠 else if (targetData.ItemID sourceData.ItemID targetData.MaxStackCount 1) { int totalAmount targetData.CurrentStackCount sourceData.CurrentStackCount; if (totalAmount targetData.MaxStackCount) { // 可以全部合并 targetData.CurrentStackCount totalAmount; items[sourceSlot.Index] null; } else { // 只能部分合并 int spaceLeft targetData.MaxStackCount - targetData.CurrentStackCount; targetData.CurrentStackCount targetData.MaxStackCount; sourceData.CurrentStackCount totalAmount - targetData.MaxStackCount; // sourceSlot 物品数量减少但依然存在 } } // 情况3: 物品不同直接交换 else { items[targetSlot.Index] sourceData; items[sourceSlot.Index] targetData; } // 数据变更后触发更新事件 OnInventoryUpdated?.Invoke(); }4.2 数据持久化保存与加载玩家退出游戏后背包数据不能丢失。序列化是必须的。序列化数据我们的ItemData类标记了[System.Serializable]但SpriteUnity引擎对象和Dictionary默认不可序列化需要特殊处理。Sprite不要直接保存Sprite。应该保存图标的资源路径如Icons/HealthPotion或其在资源管理系统如Addressables中的Key。加载时再根据这个路径或Key去加载Sprite。DictionaryUnity的JsonUtility无法直接序列化Dictionary。我们可以将其转换为ListSerializableKeyValuePair进行保存。[System.Serializable] public class SerializableItemData { public string ItemID; public string ItemName; // ... 其他基本字段 public string IconPath; // 替代 Sprite Icon public ListSerializableKeyValuePair PropertyList; // 替代 Dictionary } [System.Serializable] public class SerializableKeyValuePair { public string Key; public int Value; }选择存储方式PlayerPrefs仅适用于非常小的、简单的数据。对于背包不推荐。二进制文件使用System.IO和BinaryFormatter注意安全性和版本兼容性问题或自定义二进制格式速度快文件小。JSON/XML文件可读性好调试方便使用Newtonsoft.Json或 Unity 的JsonUtility。推荐使用JSON。数据库对于大型网游物品数据可能存储在服务器数据库如SQLite、MySQL中。保存与加载流程public void SaveInventory() { ListSerializableItemData saveList new ListSerializableItemData(); foreach (var item in items) { if (item ! null) saveList.Add(ConvertToSerializable(item)); } string json JsonUtility.ToJson(saveList); System.IO.File.WriteAllText(Application.persistentDataPath /inventory.json, json); } public void LoadInventory() { string path Application.persistentDataPath /inventory.json; if (System.IO.File.Exists(path)) { string json System.IO.File.ReadAllText(path); ListSerializableItemData loadList JsonUtility.FromJsonListSerializableItemData(json); // 将 loadList 转换回 items 数组并重新加载Icon等资源 OnInventoryUpdated?.Invoke(); } }4.3 工具提示与物品信息展示当鼠标悬停在物品上时显示一个详细的工具提示框这是提升用户体验的重要细节。创建Tooltip UI创建一个独立的TooltipPanel预制体包含背景、物品名称、描述、属性列表等Text组件。将其放在一个高层的Canvas上初始状态为隐藏。在ItemSlot中触发显示/隐藏public class ItemSlot : MonoBehaviour, IPointerEnterHandler, IPointerExitHandler { // ... 其他代码 ... public void OnPointerEnter(PointerEventData eventData) { if (currentItemUI ! null currentItemUI.ItemData ! null) { TooltipManager.Instance.ShowTooltip(currentItemUI.ItemData, transform.position); } } public void OnPointerExit(PointerEventData eventData) { TooltipManager.Instance.HideTooltip(); } }TooltipManager一个管理工具提示显示/隐藏和内容更新的单例类。它需要根据传入的ItemData动态设置文本内容并调整TooltipPanel的位置通常跟随鼠标但要注意不要超出屏幕边界。性能优化点频繁地实例化/销毁Tooltip或动态修改其布局可能引发GC垃圾回收。最好在初始化时创建好Tooltip对象通过激活/禁用和更新文本来控制显示。使用ContentSizeFitter组件可以让Tooltip根据内容自动调整大小但要注意它会在激活时强制进行布局计算可能造成卡顿。对于性能要求高的项目可以预先计算好不同物品信息的大致尺寸。5. 高级功能与性能优化一个基础的背包系统完成后我们可以考虑添加一些增强功能和进行必要的优化。5.1 背包扩展分页、分类与排序分页当物品数量过多时一次性显示所有格子会带来性能压力。可以实现分页功能通过按钮或标签切换不同的“页”。每页对应一个独立的InventoryGrid和数据切片。控制器需要管理当前页码和不同页之间的数据映射。分类与过滤添加分类按钮如“全部”、“装备”、“材料”。点击时控制器根据物品的ItemType过滤当前显示的数据列表然后刷新UI。注意这里过滤的是“显示”的数据而不是底层数据。底层数据始终保持完整。排序实现按名称、按类型、按稀有度、按获取时间等排序。在控制器中实现对items列表的排序算法然后触发OnInventoryUpdated事件。5.2 性能优化实战技巧UGUI的合批Batching对性能至关重要。不合理的UI结构会导致Draw Call激增。合批是什么简单说就是Unity将多个使用相同材质Material和纹理Texture的UI元素合并到一个Draw Call中绘制极大减少CPU向GPU发送指令的开销。破坏合批的常见原因重叠这是最容易被忽视的。如果两个UI元素如图片、文字在层级上重叠且它们之间没有父子关系通常会打断合批。尽量让需要合批的元素在矩形区域上不重叠。不同材质/纹理每个不同的物品图标都是一张独立的纹理如果它们都在同一个Canvas下且没有使用图集Atlas每个图标都会产生一个Draw Call。层级深度变化改变UI元素的层级如在拖拽时将其设为最后一个子对象可能触发Canvas的重新构建。优化策略使用图集将所有的物品图标、UI按钮背景等小图片打包成一张大图图集。这样所有使用这张大图的UI元素就可以被合批。Unity的Sprite Atlas功能可以自动帮我们做这件事。合理规划Canvas层级将动态变化的UI如拖拽图标、Tooltip和静态UI背包背景、固定按钮分开到不同的Canvas。因为Canvas下任何一个元素发生变化位置、颜色、激活状态整个Canvas都需要重新计算网格并合批。分离后拖拽图标的变化就不会导致整个背包界面重绘。避免频繁SetActive频繁激活/禁用UI游戏对象如Tooltip会产生GC和重建开销。可以改用调整CanvasGroup的Alpha和Interactable属性来“隐藏”UI或者使用对象池管理频繁出现的UI元素。谨慎使用Mask组件Mask组件以及它的升级版RectMask2D会显著增加渲染开销因为它需要额外的渲染步骤。如果只是需要滚动列表尽量使用RectMask2D它比Mask效率高。对于复杂的背包界面可以考虑自己实现简单的裁剪逻辑。5.3 与外部系统的交互背包不是孤立的它需要与角色系统、商店系统、任务系统等交互。统一的物品获取接口提供一个中心化的ItemManager它负责从配置如ScriptableObject、JSON表中根据ItemID加载ItemData模板。背包系统只持有物品的实例数据。使用事件通信当物品被使用时背包控制器抛出一个事件例如OnItemUsed(ItemData item)。角色属性系统、任务系统可以监听这个事件并做出相应反应如恢复生命、更新任务进度。这完全解耦了系统间的依赖。定义清晰的契约为背包控制器设计清晰、稳定的公共API如bool TryAddItem(ItemData item, int count)bool HasItem(string itemID, int count)。其他系统只能通过这些API与背包交互而不是直接操作其内部数据。6. 常见问题与调试技巧在实际开发中你一定会遇到各种奇怪的问题。这里记录一些我踩过的坑和解决方法。6.1 拖拽交互的“幽灵点击”与事件穿透问题在拖拽物品时如果快速点击有时会触发物品槽的点击OnPointerClick事件导致物品被意外使用。原因UGUI的事件系统在拖拽结束时如果鼠标位置没有移动可能会将其解释为点击。解决在ItemUI的OnEndDrag方法中可以检查拖拽的位移。如果位移很小eventData.delta.magnitude 5f可以认为这是一个点击并执行点击逻辑否则就只处理拖拽结束。更彻底的方法是在DragHandler中设置一个isDragging标志在拖拽期间屏蔽其他点击事件。6.2 物品数据状态的同步问题问题UI上显示的数量和背包数据列表里的数量对不上。原因最常见的原因是直接修改了ItemData的CurrentStackCount但没有通知UI更新。或者在多个地方如控制器、直接通过ItemUI脚本同时修改了数据导致状态不一致。解决严格遵守“数据驱动视图”的原则。任何对背包数据的修改必须通过控制器的公共方法如ModifyItemStack进行。在这个方法内部修改数据并立即触发OnInventoryUpdated事件。所有UI都监听这个事件来刷新自己。这样就能保证单一数据源和视图同步。6.3 滚动视图中的性能卡顿问题当背包格子非常多放在Scroll Rect里滚动时感觉不流畅。原因即使不可见的格子Unity也会对其进行布局计算和渲染准备虽然不绘制数量多了依然消耗CPU。解决实现UI虚拟列表。这是高级UI优化的必备技能。原理是只创建足够覆盖可视区域的格子数量比如一屏能显示20个就创建25个作为缓冲。当滚动时动态回收滚出屏幕的格子并重新利用它们来显示新进入屏幕的数据。Unity官方没有直接提供此组件但Asset Store有优秀插件如EnhancedScroller也可以自己基于Scroll Rect和Content Size Fitter实现。6.4 内存管理与资源加载问题打开背包时卡顿或者切换场景后背包图标变成粉色丢失材质。原因物品图标等资源加载策略不当。如果在每次刷新背包时都同步加载Sprite会造成卡顿。如果资源引用管理不当可能导致资源被意外卸载。解决异步加载使用Addressables或Resources.LoadAsync来异步加载图标资源。在ItemUI.Setup方法中可以先显示一个占位图然后发起异步加载请求加载完成后再替换为真实图标。引用管理确保只要背包数据中存在某个物品其对图标资源的引用就保持有效防止被垃圾回收或Addressables系统卸载。对于从Resources加载的资源Unity会自动管理对于Addressables需要使用AsyncOperationHandle来保持引用。6.5 输入冲突UI与游戏场景的点击问题当背包UI打开时点击UI后面的游戏场景仍然触发了场景中的物体选择或角色移动。原因Graphic Raycaster不会阻挡投射到3D或2D物理物体的射线。解决有几种方法打开背包时禁用玩家输入或切换输入状态。在背包Canvas上添加一个全屏的、透明的Image作为背景并确保其Raycast Target为true。它可以拦截所有点击事件防止穿透到场景。使用EventSystem.current.IsPointerOverGameObject()方法在玩家的点击逻辑中判断是否点击在了UI上如果是则不处理场景点击。构建一个完整的UGUI背包系统是一次对Unity UI架构、数据管理和软件设计模式的综合练习。从简单的格子排列到复杂的数据驱动架构每一步都考验着开发者对细节的把握和对扩展性的预见。记住没有“最好”的设计只有“最适合”当前项目需求的设计。开始时可以快速实现一个可用的版本但随着功能增加要有意识地进行重构向更清晰、更解耦的方向演进。希望这篇详尽的拆解能帮你避开我当年踩过的那些坑更顺畅地打造出属于你自己的游戏背包。
返回列表