垃圾分类游戏在儿童教育、社区科普和环保知识普及里一直很有市场,我身边不少开发者朋友接到的第一个小游戏需求就是它。做这类项目,交互方案通常有两种思路:一种是拖拽垃圾到对应垃圾桶,另一种就是点击按钮分类。我这次要分享的是后者——Unity2D 垃圾分类的实现(按钮实现),这种方案更稳妥、更省事,尤其在触屏设备和教学场景下,误触率低,代码逻辑也更好维护。
这篇文章适合刚接触 Unity UI 开发的初学者,也适合想快速给项目加一个垃圾分类玩法模块的开发者。我会从整体设计、核心逻辑、实操代码到踩坑排查,完整走一遍。整个项目用到的核心知识点就三块:Button 组件的事件绑定、数据驱动的分类逻辑、UI 反馈与计分管理。这三块吃透了,你不仅能做垃圾分类,还能套用到答题闯关、知识竞猜这类点击交互的小游戏上。
1. 项目整体设计与方案选型
1.1 为什么选按钮交互,不选拖拽
先说结论:按钮方案是垃圾分类小游戏性价比最高的实现方式,没有之一。
拖拽方案听起来更炫,但上手就一堆事。你要处理IDragHandler、IBeginDragHandler这些接口,要考虑垃圾图标被拖到垃圾桶上方时的碰撞检测或者坐标范围判断,还要管拖拽对象和 UI 层级的关系。最麻烦的是在手机上,手指会挡住垃圾图标,玩家根本看不清自己拖的是啥,体验很容易翻车。
按钮方案就不一样了。中间一块区域显示当前垃圾的图标和名称,下面一排四个分类按钮,分别是可回收垃圾、有害垃圾、厨余垃圾、其他垃圾。玩家看一眼垃圾,点一下对应按钮,系统立刻判定对错并给反馈,整个过程没有拖泥带水的操作。这种设计特别适合小朋友使用,也适合用鼠标操作的 PC 课堂场景。
还有个现实原因:按钮方案的代码量大概只有拖拽方案的三分之一,出 bug 的概率也低得多。Unity 自带的Button组件非常成熟,点一下绑定事件,不需要处理任何坐标换算。如果你项目排期紧、又要保证稳定性,按钮方案几乎是必然选择。
1.2 四类垃圾分类的判定逻辑
垃圾分类的核心规则一定要先理清楚,这部分不能含糊。国内通用的标准是四大类:可回收垃圾、有害垃圾、厨余垃圾、其他垃圾。
- 可回收垃圾:废纸、塑料瓶、玻璃瓶、金属罐、旧衣服。核心特征是“可以卖钱、可再生”。
- 有害垃圾:废电池、过期药品、废灯管、油漆桶。核心特征是“有毒性、需特殊处理”。
- 厨余垃圾:剩菜剩饭、果皮、蛋壳、茶叶渣。核心特征是“易腐烂、可堆肥”。
- 其他垃圾:烟蒂、陶瓷碎片、用过的纸巾、一次性餐具。核心特征是“难以回收、无害但不可再生”。
游戏里,每种垃圾对应一个枚举类型。例如下面这样:
public enum GarbageType { Recyclable, // 可回收 Hazardous, // 有害 Kitchen, // 厨余 Other // 其他 }判定逻辑非常简单:玩家点击某个按钮,按钮携带自己的类型,跟当前垃圾的类型比对,相等就是正确,不等就是错误。但这里有一个设计要点:按钮要知道自己是哪个类型,而不是让逻辑代码写死四个按钮分别干嘛。这样以后想加第五类、第六类垃圾,只需要扩展枚举和新增按钮,不需要改动核心判断代码。
1.3 项目结构规划
我习惯做小游戏也按“数据–表现–控制”三层来拆,这样项目越大越不容易乱。整个模拟项目 X 的目录结构大概是这样的:
Assets/ ├── Scripts/ │ ├── Core/ │ │ ├── GarbageData.cs // 垃圾数据模型 │ │ ├── GarbageType.cs // 类型枚举 │ │ └── GarbageDatabase.cs // 垃圾题库 │ └── UI/ │ ├── GameManager.cs // 游戏主控 │ └── UIManager.cs // UI 更新与反馈 ├── Prefabs/ │ └── GarbageIcon.prefab // 垃圾图标预制体 ├── Resources/ │ └── Icons/ // 各类垃圾的图片素材 └── Scenes/ └── Main.unity数据、表现、控制分开之后,换图、换题、改 UI 都不会互相牵连。尤其是垃圾题库独立成一个数据类,后面想扩充题库只需要往列表里加数据,完全不用动其他代码。
2. 核心功能拆解与 UI 布局设计
2.1 UI 的场景布局与层级关系
整个游戏场景的 UI 布局我建议搭三层 Canvas,不要全堆在一个 Canvas 里,否则后期改层级会很痛苦。
第一层叫GameCanvas,放的是主界面元素:背景图、标题、当前垃圾显示区、四个分类按钮、得分文本、生命值文本。这些是游戏最核心的交互元素。
第二层叫FeedbackCanvas,放的是正确/错误的提示图标。单独放在一层的原因是,它有动画(放大淡出之类的),会临时覆盖在游戏界面上,如果跟主界面同一个层级,动画播放时容易被其他 UI 遮挡。
第三层叫OverlayCanvas,放的是游戏结束弹窗。这个弹窗带半透明背景遮罩,单独一层最合适,不会被任何东西挡住。
具体每个按钮的尺寸和间距,我直接给一组可用的参数:四个分类按钮建议等宽,宽度 240、高度 100,水平排列,间距 20。按钮字体不要小于 36,儿童向的游戏字号尽量大。垃圾图标显示区放在按钮上方,占屏幕中间偏上位置,图标大小控制在 256×256 以内,太大了会显得笨重。
2.2 垃圾题库的设计
垃圾题库是整个游戏的数据基础。我设计了一个GarbageDatabase类,里面分了四个列表,每个列表存对应类型的垃圾数据。数据结构长这样:
[System.Serializable] public class GarbageData { public string garbageName; // 垃圾名称 public GarbageType type; // 垃圾类型 public Sprite icon; // 垃圾图标 }题库初始化的时候,我用了四个List<GarbageData>,分别存放四类垃圾:
public class GarbageDatabase { public List<GarbageData> recyclableList = new List<GarbageData>(); public List<GarbageData> hazardousList = new List<GarbageData>(); public List<GarbageData> kitchenList = new List<GarbageData>(); public List<GarbageData> otherList = new List<GarbageData>(); }你可以在Start()方法里手动往每个列表塞数据,比如:
recyclableList.Add(new GarbageData { garbageName = "塑料瓶", type = GarbageType.Recyclable, icon = Resources.Load<Sprite>("Icons/Recyclable/plastic_bottle") }); hazardousList.Add(new GarbageData { garbageName = "废电池", type = GarbageType.Hazardous, icon = Resources.Load<Sprite>("Icons/Hazardous/battery") });如果项目数据量大,建议直接用 ScriptableObject 做配置表,在 Inspector 里拖拽赋值,会直观很多。但小 Demo 用代码初始化就够了,别过度设计。
2.3 随机出题的策略
出题逻辑是核心体验的一环。一开始我犯过一个错误:直接从四个列表里随机选一个列表,再从该列表随机选一条数据。这样导致的结果是,每类垃圾出现概率固定在 25%,看起来不均匀——玩家很容易感觉到“怎么老是出电池”。
正确做法是:维持一个出题池,每轮从题目池随机抽,抽中后从池里移除,池空了再初始化一轮。这样可以保证短时间内垃圾类型尽量不重复。
我当时的实现是:
private List<GarbageData> questionPool = new List<GarbageData>(); void ResetPool() { questionPool.Clear(); questionPool.AddRange(recyclableList); questionPool.AddRange(hazardousList); questionPool.AddRange(kitchenList); questionPool.AddRange(otherList); } GarbageData GetNextGarbage() { if (questionPool.Count == 0) ResetPool(); int index = Random.Range(0, questionPool.Count); GarbageData data = questionPool[index]; questionPool.RemoveAt(index); return data; }另外注意一个细节:Random.Range的 int 重载是包含最小值、不包含最大值的,所以Random.Range(0, questionPool.Count)不会取到 Count,不会越界。这个坑虽然小,但我见过不少新手在这里栽跟头,值得提一嘴。
3. 按钮事件的绑定与核心逻辑实现
3.1 四种按钮事件的绑定方式
按钮点击事件的绑定有两种主流做法:Inspector 手动拖拽绑定和代码动态绑定。
手动拖拽的方式很简单:选中按钮,在 Inspector 窗口的 Button 组件下找到On Click()列表,点击加号,把挂载脚本的对象拖进去,选择方法。这种方式适合按钮数量固定、功能明确的场景。
但这个项目里,我推荐代码动态绑定。原因是四个按钮的逻辑极其相似,都是“判断类型 + 触发分类”,写循环绑定比手动拖四次更干净,也更方便扩展。
所以我当时是在GameManager的Start()里统一绑定的:
Button[] buttons = new Button[4]; buttons[0] = recyclableBtn; buttons[1] = hazardousBtn; buttons[2] = kitchenBtn; buttons[3] = otherBtn; for (int i = 0; i < buttons.Length; i++) { GarbageType type = (GarbageType)i; // 枚举顺序和按钮顺序保持一致 buttons[i].onClick.AddListener(() => PlayerChoose(type)); }这里有个关键点:lambda 表达式里不能直接用循环变量 i,因为闭包捕获的是同一个 i 的引用,循环结束后 i 都是 4,四个按钮绑定的事件拿到的全是最后一个值。一定要先复制一份局部变量。我上面的写法直接用了type这个循环体内的新变量,就绕开了这个问题。这个坑,Unity 开发者几乎每个人都踩过,我第一次写的时候也排查了半天。
3.2 判断对错与状态切换
按钮按下去之后,PlayerChoose(GarbageType type)要做的第一件事是判断玩家点击的类型是否等于当前垃圾的类型:
void PlayerChoose(GarbageType chosenType) { if (isGameOver) return; if (chosenType == currentGarbage.type) { OnAnswerCorrect(); } else { OnAnswerWrong(); } }关于答错之后的处理,这里有两种产品设计思路,要看你的游戏定位。
第一种是“答错不换题”,玩家答错了提示错误,当前垃圾保留,让玩家重新选。这种设计偏向教学,给玩家思考和试错的机会,适合儿童科普类场景。
第二种是“答错换下一题”,答错了扣分或者扣生命值,然后自动换一个垃圾。这种设计偏向挑战和闯关,节奏更快,适合竞技或街机风格。
我做的是教学向,所以选了第一种:答错只提示,不扣生命值,但也不加分,直到玩家选对才进入下一题。如果你想做挑战模式,很简单,把答错分支改成扣除lifeCount并且调用GetNextGarbage()加载下一题就行。
3.3 正确与错误的反馈机制
反馈是游戏体验的灵魂。按钮按下去如果只是换个分数数字,玩家会觉得很干。我准备了四个反馈层级的方案,从轻到重:
- 即时视觉反馈:按钮按下去的瞬间,正确显示绿色对勾并淡入淡出,错误显示红色叉并左右晃动。
- 分数变化反馈:得分文本用 DOTween 做一个 punch 缩放,加分时放大一下,让玩家有“得分了”的爽感。
- 声音反馈:正确播放清脆的叮咚声,错误播放短促的嗡鸣声。这个很有效,但对游戏体验的提示作用比视觉更直接。
- 连续正确奖励:连续答对 5 题触发“连击”提示,额外加 10 分。这个能让玩家更有动力集中注意力,不至于乱点。
我这里展示一下核心的反馈代码,动画部分我用的是协程 +LeanTween或 DOTween,下面是一个用 DOTween 的示例:
void ShowFeedback(bool isCorrect) { if (isCorrect) { correctIcon.gameObject.SetActive(true); correctIcon.DOScale(Vector3.one * 1.2f, 0.2f).OnComplete(() => { correctIcon.DOScale(Vector3.one, 0.15f); correctIcon.GetComponent<CanvasGroup>()?.DOFade(0, 0.3f); }); scoreText.DOPunchScale(Vector3.one * 1.3f, 0.3f, 10, 1); } else { wrongIcon.gameObject.SetActive(true); wrongIcon.DOShakeAnchorPos(0.4f, 30, 20, 90); } }如果不想引入动画插件,用协程写一个简单的 IO 效果也完全没问题。核心是“有反馈、且反馈有层次”,不要只改一个数字就完事。我实测下来,加入这些反馈后,给小朋友试玩,他们的注意力和参与度明显高了不止一个档次。
3.4 得分与生命值管理
得分和生命值的管理最好用一个独立的游戏状态类,不要什么变量都丢在GameManager里,否则后面代码越来越乱。我的状态管理长这样:
[System.Serializable] public class GameState { public int score; public int comboCount; public int lifeCount; public int totalCount; public void Reset() { score = 0; comboCount = 0; lifeCount = 3; totalCount = 0; } }GameManager持有GameState的引用。答对时增加score和comboCount,答错时comboCount清零。当comboCount达到 5 的倍数时,额外加分。totalCount是总答题数,可以用在结束界面统计正确率。
这里有个实用小设计:答题总数达到某个值(比如 20 题)就触发游戏结束,而不是靠生命值扣完。因为教学向游戏里,如果一直答对,生命值永远扣不完,游戏就没完没了。设定一个固定总题数,每道题无论对错都推进进度,节奏更可控。我在项目里用的是双条件:答对 20 题通关,或者生命值归零失败,先触发哪个算哪个。
4. 数据与场景集成:让游戏完整跑起来
4.1 垃圾显示区的刷新与状态维护
当前垃圾的显示刷新,我放在了UpdateGarbageDisplay(GarbageData data)方法里,统一处理图标、名称文本和答题状态的初始化。
简单说,这个方法要做三件事:更新图标 sprite、更新名称文本、重置按钮的交互状态。如果答错不换题,还需要在答错时置灰选错的按钮,防止玩家揪着一个按钮狂点。
置灰按钮的实现如下。每个按钮对应一个类型,如果它不等于正确答案类型,并且已经被点错一次,就把它变成灰色切不可点击:
void LockWrongButton(GarbageType wrongType) { Button targetBtn = GetButtonByType(wrongType); targetBtn.interactable = false; targetBtn.image.color = Color.gray; }这里有个容易忽略的点:New 一题时必须把所有按钮重新激活,否则上一题锁掉的按钮会带到下一题,导致玩家根本没机会点它。我在UpdateGarbageDisplay()里统一重置:
void ResetAllButtons() { foreach (Button btn in allTypeButtons) { btn.interactable = true; btn.image.color = originalColor; } }这个细节看起来不起眼,但真的很影响游戏体验。我第一次做完实测的时候,就发现答错一道题之后,后面所有题目里那个按钮都点不了,排查了半天才找到是忘了重置按钮状态。你如果也踩到这个坑,直接看这个函数就行。
4.2 素材命名与 Resources 加载规范
游戏里的垃圾图片素材,我统一放在Assets/Resources/Icons/目录下,并按垃圾类型分四个子文件夹:Recyclable、Hazardous、Kitchen、Other。
文件命名我建议统一用小写加下划线,例如plastic_bottle.png、waste_battery.png。命名规范的好处有两个:一是Resources.Load路径不容易出错,二是以后如果要做批量导入或者跟配置表对接,规范的命名能省很多事情。
用Resources.Load加载图片时有个坑:图片的 Texture Type 必须是 Sprite (2D and UI),否则加载出来后赋给 Image.sprite 会报错或者显示空白。解决办法是在 Project 窗口选中图片,把 Inspector 里的Texture Type改为Sprite (2D and UI)。如果你有成批图片要处理,全选图片一次改就行,不用一张一张来。
4.3 从预制体实例化垃圾展现
还有一种更高级的做法:把垃圾图标做成预制体,在场景里实例化。这样做的好处是每个垃圾可以挂独立的脚本,以后想给某种垃圾加特殊效果(比如金属类垃圾点击时闪一下光效),只需要改预制体就行。
我这个项目的做法是:垃圾显示区放一个空节点GarbageDisplayRoot,每次出题时,把旧的子物体销毁,再实例化新的垃圾图标预制体,挂到显示区下:
GameObject iconGo = Instantiate(garbageIconPrefab, displayRoot.transform); iconGo.GetComponent<Image>().sprite = currentGarbage.icon; iconGo.GetComponentInChildren<Text>().text = currentGarbage.garbageName;注意,每次实例化之前,记得先把旧的子物体清理掉,不然新旧图标会叠在一起。我当时写的时候偷懒没做清理,结果出现了一个很滑稽的 bug:屏幕上叠了五六个图标,新的盖在旧的上面,还能隐约看到下面几层,调试的时候我一度以为是实现了“残影特效”。
5. 常见问题与排查技巧实录
5.1 按钮点击没反应
按钮点击没反应,是新手遇到最多的问题。按照下面这个顺序排查,基本百分百能解决:
第一步,检查场景里是不是有EventSystem。没有EventSystem,所有 UI 交互都会失效。新建场景默认会带,但如果你手动创建场景反而容易漏。这是最低级的错误,但也是最常见的。
第二步,检查 Canvas 上是不是有Graphic Raycaster组件。没有这个组件,射线检测不到 UI 元素,按钮自然点不动。
第三步,检查按钮的Image组件是不是被隐藏或者透明度为 0。如果Image不可见,射线检测可能根本不会通过,除非你勾选了Raycast Target。这里有个表层逻辑:按钮的可点击区域依赖于Image的 alpha,alpha 为 0 且不勾选 Raycast Target 时就是点击不到。
第四步,检查按钮和显示垃圾的区域有没有被其他 UI 挡住。有可能你在界面上放了一个全屏的半透明背景图,它放在了按钮上层,把按钮的点击事件全吞了。处理方式是调整层级,或者把背景图的Raycast Target勾选去掉。
第五步,检查按钮绑定的方法是不是被代码动态绑定了两遍。比如你在Awake里绑了一次,Start里又绑了一次,点击一次会触发两次回调,表现就是分数一次性加 2 或者提示闪两次。解决办法是绑定时先移除再添加,或者在OnDisable时移除监听。
要说明一下:Unity 的 EventSystem 存在时,Button.onClick的调用走的就是 UI 射线检测 + 事件系统,这是它和普通 GameObject 按钮(比如你用 OnMouseDown 却挂在 UI 上)最直接的区别。如果你为了省事把 UI 上的按钮挂了个OnMouseDown(),在 PC 上可能碰巧能触发,但发到手机上就会非常不稳定。不要混用这两套交互体系。
5.2 lambda 闭包陷阱导致所有按钮绑定同一个类型
这是这个项目最容易出现的代码级 bug,我上面已经提过,但这里还是要单独拎出来强调一遍,因为太典型了。
错误写法:
for (int i = 0; i < 4; i++) { buttons[i].onClick.AddListener(() => PlayerChoose((GarbageType)i)); }这样写,四个按钮点击之后全部调用PlayerChoose((GarbageType)4),而 4 不是有效枚举值,判断必然出错。因为 C# 的闭包捕获的是变量 i 的引用,循环结束之后 i 的值是 4。
正确写法之一:
for (int i = 0; i < 4; i++) { GarbageType localType = (GarbageType)i; buttons[i].onClick.AddListener(() => PlayerChoose(localType)); }正确写法之二:
buttons[0].onClick.AddListener(() => PlayerChoose(GarbageType.Recyclable)); buttons[1].onClick.AddListener(() => PlayerChoose(GarbageType.Hazardous)); buttons[2].onClick.AddListener(() => PlayerChoose(GarbageType.Kitchen)); buttons[3].onClick.AddListener(() => PlayerChoose(GarbageType.Other));如果你已经写了错误写法,调试的时候会发现一个很诡异的现场:不管点什么按钮,反馈都是错误,而且提示错误时用的总是某一个固定类型。这时候不用犹豫,直接检查循环闭包。
5.3 答题中途按钮状态残留
这个坑我也踩过一次。答错锁按钮功能做出来后,有一阵子发现第二题开始,有些按钮明明是正确的,却点不了,颜色还是灰色的。查了半天发现是ResetAllButtons()只在换新题的时候调用,而当时代码里答对调一次换题、答错调一次锁定,状态切换的路径没走对。
排查经验分享给你:只要看到按钮状态跟预期不符,先别急着查逻辑,直接在UpdateGarbageDisplay()入口处加一行ResetAllButtons(),把状态重置提前到所有路径的公用入口。这样哪怕某条分支忘写了,最后也会被统一重置兜底。
5.4 手机分辨率适配
按钮方案做手机适配比拖拽方案容易得多,但也不是完全不用管。我用的适配方案是 Canvas Scaler 设为Scale With Screen Size,参考分辨率设成1080x1920,匹配模式为Match Width Or Height,并设匹配系数为 0.5。
这样在大部分手机上面 UI 比例都正常。如果你要适配横屏,参考分辨率改成1920x1080就行。
一个容易忽略的小点是:底部四个按钮在大屏手机上可能会被系统手势区遮挡。解决办法有两种,一是把按钮区域内边距调大(底部留出 100 像素以上安全区),二是用屏幕安全区 API 动态调整 Canvas 的边距。教学 Demo 用第一种就够,自己上架的话建议做安全区适配。
6. 体验优化与扩展方向
6.1 音效与视觉细节打磨
音效这块,我不建议在这个阶段花太多时间找素材,直接用代码生成简单的音效就够了。正确音效可以用一个短促的上升音阶,错误音效用低频的“嗡”声。如果你不会编写音频代码,就用 Unity 商店里免费的音效包,一个叮咚声一个错误声,放在 UI 点击时播放就行。
视觉上,四类垃圾对应的按钮最好用不同的颜色,并且颜色要符合直觉:可回收用蓝色、有害用红色、厨余用绿色、其他用灰色。这样玩家不需要读文字,只看颜色就能快速定位想要点击的按钮,对儿童用户尤其友好。
配色这一块我在第一个版本里踩过坑:一开始四个按钮全是同一个基础色,只有文字不一样。小朋友玩的时候经常看错按钮,点错了还一脸委屈。后来改成四个颜色之后,准确率肉眼可见地提升了。做教育类游戏,界面本身也是教学内容的一部分。
6.2 题库扩充:从固定列表到资源动态加载
如果你的题库想越做越大,几百条数据都写在代码里就不合适了。推荐用两种方式:一种是 JSON 配置表,一种是 ScriptableObject 资源。
用 JSON 的思路是,把垃圾名称和类型 ID 写在一个 JSON 文件里,游戏初始化时读取并解析,图标通过名称拼路径动态加载:
[ { "name": "塑料瓶", "type": 0, "icon": "Icons/Recyclable/plastic_bottle" }, { "name": "废电池", "type": 1, "icon": "Icons/Hazardous/waste_battery" } ]然后反序列化:
List<GarbageData> allData = JsonConvert.DeserializeObject<List<GarbageData>>(jsonText);用了这套方案之后,你更新题库只需要改配置文件,完全不用动的程序代码。对非程序员的内容配置人员来说,这体验非常友好。
6.3 从答题器到完整小游戏的扩展思路
按钮答完题判分,这只是最基本的一层玩法。如果你想让这个项目有更多可玩性,可以考虑下面几个扩展方向:
- 关卡制:按难度分组,第一关只有可回收和厨余两类,第二关加入有害,第三关四类全上。这样玩家学习曲线更平滑。
- 限时模式:每道题 10 秒倒计时,倒计时结束自动判定错误。游戏的紧张感一下就上来了。
- 多人 PK:两台设备比拼正确率。这个扩展建议用局域网同步或者分屏,代码层面只要把判分逻辑抽成公共接口就行。
- 科普知识弹窗:答完题弹出一个气泡,展示该垃圾的详细科普信息,比如“塑料瓶回收之后可以制成涤纶布料”。这个功能特别适合社区宣传场景,教育价值非常高。
我在实际项目里就加了科普弹窗,答题正确后显示一段话,结果用户停留时长提高了不少。很多时候,玩家玩垃圾分类游戏不只是为了得分,是真的想学点东西。与其做完题直接跳下一题,不如借机把知识嚼碎了喂给玩家。
7. 结尾一点实际感想
做这个 Unity2D 垃圾分类项目,回头看最大的心得就是:按钮方案看起来简单,但正因为简单,代码的每一步都必须写得干净、清晰、可扩展。一个负责的按钮点击事件背后,是数据模型、类型枚举、题库管理、状态管理、动画反馈这一整套逻辑的配合。
我个人在实际操作中体会到的一点是:小游戏最容易出问题的往往不是复杂的玩法逻辑,而是按钮状态管理、事件绑定这些基础细节。如果你在这个项目里能把这些基础功练扎实,后面做什么类型的 UI 交互游戏都会顺手很多。
最后再分享一个小技巧:如果你想快速测试自己的判断逻辑对不对,可以做一个 Debug 面板,在游戏界面上实时显示当前的currentGarbage.type枚举名。这个方法帮我节省了大量测试时间,比盯着垃圾图标猜类型要高效得多。
如果你按这篇文章的步骤做完一遍,遇到问题也可以回头看看第 5 节的问题清单,绝大多数坑都在里面了。希望这个项目能成为你 Unity UI 开发路上的一个扎实起点。